When you’re working with data in Go, especially when you need to handle dynamic inputs like JSON from third-party APIs, understanding how to properly convert between data types is key. This helps you avoid bugs and crashes.
Often times, the values returned by APIs are stored as generic interface{}
types. These require explicit typecasting to use them correctly. But without proper type conversion, you risk data loss, unexpected behavior, or even runtime crashes.
In this article, we’ll explore how typecasting works in Go. You’ll learn what it is, how to do it correctly, and why it’s crucial for writing safe and reliable code.
You’ll learn about implicit vs explicit typecasting, common pitfalls to avoid, and how to safely work with dynamic data. We’ll also cover practical examples, including how to handle JSON data and how Go’s new generics feature can simplify type conversions.
Table of Contents:
Why You Should Care About Typecasting
I decided to write about this after running into a real issue in a company’s codebase. The app was pulling data from a third-party API that returned JSON objects. The values were dynamic and stored as generic interface{}
types, but the code was trying to use them directly as int
, float64
, and string
without checking or converting the types properly. This caused silent bugs, unexpected behavior, and even crashes that took hours to trace back.
If you’re learning Go – or any language – knowing when and how to typecast can save hours of debugging. So let’s get into it.
What Is Typecasting?
Typecasting (or type conversion) is when you convert one type of variable into another. For example, turning an int
into a float
, or a string
into a number. It’s a simple but essential technique for working with data that doesn’t always come in the type you expect.
There are two main types of typecasting:
Implicit (automatic): Happens behind the scenes, usually when it’s safe (for example,
int
tofloat64
in some languages).Explicit (manual): You, the developer, are in charge of the conversion. This is the case in Go.
Why does this matter? Because if you don’t convert types correctly, your program might:
Lose data (for example, decimals getting cut off).
Crash unexpectedly.
Show incorrect results to users.
I share some resources at the end of the article if you’re looking for Go packages that simplify type conversions and reduce boilerplate.
How to Typecast in Go
Go is a statically typed language, and it doesn’t do implicit conversions between different types. If you want to change a type, you have to do it yourself using explicit syntax.
Let’s look at some basic examples:
<span class="hljs-keyword">var</span> a <span class="hljs-keyword">int</span> = <span class="hljs-number">42</span> <span class="hljs-comment">// Declare a variable 'a' of type int and assign the value 42</span>
<span class="hljs-keyword">var</span> b <span class="hljs-keyword">float64</span> = <span class="hljs-keyword">float64</span>(a) <span class="hljs-comment">// Explicitly convert 'a' from int to float64 and store it in 'b'</span>
<span class="hljs-comment">// Go requires manual (explicit) type conversion between different types</span>
Here, we’re converting an int
(a
) into a float64
(b
). This is a widening conversion – it’s safe because every integer can be represented as a float.
Now the reverse:
<span class="hljs-keyword">var</span> x <span class="hljs-keyword">float64</span> = <span class="hljs-number">9.8</span> <span class="hljs-comment">// Declare a float64 variable 'x' with a decimal value</span>
<span class="hljs-keyword">var</span> y <span class="hljs-keyword">int</span> = <span class="hljs-keyword">int</span>(x) <span class="hljs-comment">// Convert 'x' to an int and store it in 'y'</span>
<span class="hljs-comment">// This removes (truncates) everything after the decimal point</span>
<span class="hljs-comment">// So y will be 9, not 10 — it doesn't round!</span>
Here, we convert a float64
to an int
, which truncates the decimal part. This is a narrowing conversion and can lead to data loss.
Go forces you to be explicit so you don’t accidentally lose information or break your logic.
Common Mistakes to Avoid
When working with dynamic data like JSON or third-party APIs, it’s common to use interface{}
to represent unknown types. But you can’t directly use them as specific types without checking first.
Here’s a mistake many beginners make:
<span class="hljs-keyword">var</span> data <span class="hljs-keyword">interface</span>{} = <span class="hljs-string">"123"</span> <span class="hljs-comment">// 'data' holds a value of type interface{} (a generic type)</span>
value := data.(<span class="hljs-keyword">string</span>) <span class="hljs-comment">// This tries to assert that 'data' is a string</span>
<span class="hljs-comment">// If it's not a string, this will panic and crash the program</span>
If data
isn’t actually a string
, this will panic at runtime.
A safer version would be:
value, ok := data.(<span class="hljs-keyword">string</span>) <span class="hljs-comment">// Try to convert 'data' to string, safely</span>
<span class="hljs-keyword">if</span> !ok {
fmt.Println(<span class="hljs-string">"Type assertion failed"</span>) <span class="hljs-comment">// If the type doesn't match, 'ok' will be false</span>
} <span class="hljs-keyword">else</span> {
fmt.Println(<span class="hljs-string">"Value is:"</span>, value) <span class="hljs-comment">// Only use 'value' if assertion was successful</span>
}
This checks the type before converting and avoids a crash. Always handle the ok
case when asserting types from interface{}
.
A Real-World Example: Where Things Go Wrong
We will be using a lot of the JSON marshal and unmarshal functions. If you want to understand what these are, here’s a quick introduction or review.
What is Marshaling in Go?
Marshaling refers to the process of converting Go data structures into a JSON representation. This is especially useful when you’re preparing data to be sent over the network or saved to a file. The result of marshaling is typically a byte slice containing the JSON string.
Unmarshaling, on the other hand, is the reverse operation. It converts JSON data into Go structures, allowing you to work with external or dynamic data formats in a strongly typed manner.
In typical applications, you might marshal a struct to send data via an API, or unmarshal a JSON payload received from a third-party service.
When using structs, marshalling and unmarshalling are straightforward and benefit from field tags that guide JSON key mapping. But when working with unstructured or unknown JSON formats, you might unmarshal into a map[string]interface{}
. In these cases, type assertions become necessary to safely access and manipulate the data.
Understanding how marshalling and unmarshalling work is fundamental when building services that consume or expose APIs, interact with webhooks, or deal with configuration files in JSON format.
Alright, now back to our example:
Let’s say you get a JSON response from an API and unmarshal it into a map:
<span class="hljs-keyword">package</span> main
<span class="hljs-keyword">import</span> (
<span class="hljs-string">"encoding/json"</span>
<span class="hljs-string">"fmt"</span>
)
<span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">main</span><span class="hljs-params">()</span></span> {
data := []<span class="hljs-keyword">byte</span>(<span class="hljs-string">`{"price": 10.99}`</span>) <span class="hljs-comment">// Simulated JSON input</span>
<span class="hljs-keyword">var</span> result <span class="hljs-keyword">map</span>[<span class="hljs-keyword">string</span>]<span class="hljs-keyword">interface</span>{} <span class="hljs-comment">// Use a map to unmarshal the JSON</span>
json.Unmarshal(data, &result) <span class="hljs-comment">// Unmarshal into a generic map</span>
price := result[<span class="hljs-string">"price"</span>].(<span class="hljs-keyword">float64</span>) <span class="hljs-comment">// Correctly assert that price is a float64</span>
fmt.Println(<span class="hljs-string">"The price is:"</span>, price)
total := <span class="hljs-keyword">int</span>(result[<span class="hljs-string">"price"</span>]) <span class="hljs-comment">// ❌ This will fail!</span>
}
This fails because result["price"]
is of type interface{}
. Trying to convert it directly to int
causes a compile-time error:
cannot convert result[“price”] (map index expression of type interface{}) to type int: need type assertion
You need to assert the type first.
The Right Way to Do It
Here’s the safe and correct version:
<span class="hljs-keyword">package</span> main
<span class="hljs-keyword">import</span> (
<span class="hljs-string">"encoding/json"</span>
<span class="hljs-string">"fmt"</span>
)
<span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">main</span><span class="hljs-params">()</span></span> {
data := []<span class="hljs-keyword">byte</span>(<span class="hljs-string">`{"price": 10.99}`</span>) <span class="hljs-comment">// JSON input representing a float value</span>
<span class="hljs-keyword">var</span> result <span class="hljs-keyword">map</span>[<span class="hljs-keyword">string</span>]<span class="hljs-keyword">interface</span>{} <span class="hljs-comment">// Create a map to hold the parsed JSON</span>
json.Unmarshal(data, &result) <span class="hljs-comment">// Parse the JSON into the map</span>
<span class="hljs-comment">// Step 1: Assert that the value is a float64</span>
priceFloat, ok := result[<span class="hljs-string">"price"</span>].(<span class="hljs-keyword">float64</span>)
<span class="hljs-keyword">if</span> !ok {
fmt.Println(<span class="hljs-string">"Failed to convert price to float64"</span>)
<span class="hljs-keyword">return</span>
}
fmt.Println(<span class="hljs-string">"Total as float:"</span>, priceFloat) <span class="hljs-comment">// Successfully extracted float value</span>
<span class="hljs-comment">// Step 2: Convert the float to an int (truncates decimals)</span>
total := <span class="hljs-keyword">int</span>(priceFloat)
fmt.Println(<span class="hljs-string">"Total as integer:"</span>, total) <span class="hljs-comment">// Final integer result (e.g., 10 from 10.99)</span>
}
This works because we first check that the value is a float64
and only then convert it to an int
. That two-step process – type assertion then conversion – is key to avoiding errors.
Advanced: How to Use Generics for Safer Typecasting
With the introduction of generics in Go 1.18, you can write reusable functions that work with any type. Generics let you define functions where the type can be specified when the function is called.
What are Generics in Go?
Generics were introduced in Go 1.18 to allow writing functions and data structures that work with any type. They help reduce code duplication and increase type safety by enabling parameterized types.
In the context of typecasting, generics allow you to write flexible helpers (like getValue[T]
) that reduce repetitive interface{}
assertions and make your code easier to maintain.
Type parameters are defined with square brackets:
[T any]
The
any
keyword is an alias forinterface{}
Compile-time checks ensure the past types are used safely
Generics are especially useful in libraries, APIs, and when working with dynamic structures like JSON objects.
Let’s say you want to extract values from map[string]interface{}
without writing repetitive assertions:
<span class="hljs-comment">// A generic function that safely retrieves and type-asserts a value from a map</span>
<span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">getValue</span>[<span class="hljs-title">T</span> <span class="hljs-title">any</span>]<span class="hljs-params">(data <span class="hljs-keyword">map</span>[<span class="hljs-keyword">string</span>]<span class="hljs-keyword">interface</span>{}, key <span class="hljs-keyword">string</span>)</span> <span class="hljs-params">(T, <span class="hljs-keyword">bool</span>)</span></span> {
val, ok := data[key] <span class="hljs-comment">// Check if the key exists in the map</span>
<span class="hljs-keyword">if</span> !ok {
<span class="hljs-keyword">var</span> zero T <span class="hljs-comment">// Declare a zero value of type T</span>
<span class="hljs-keyword">return</span> zero, <span class="hljs-literal">false</span> <span class="hljs-comment">// Return zero value and false if key not found</span>
}
converted, ok := val.(T) <span class="hljs-comment">// Try to convert (type assert) the value to type T</span>
<span class="hljs-keyword">return</span> converted, ok <span class="hljs-comment">// Return the result and success status</span>
}
This function:
Accepts any type
T
that you specify (likefloat64
,string
, and so on)Asserts the type for you
Returns the value and a boolean indicating success
Usage:
price, ok := getValue[<span class="hljs-keyword">float64</span>](result, <span class="hljs-string">"price"</span>) <span class="hljs-comment">// Try to get a float64 from the map</span>
<span class="hljs-keyword">if</span> !ok {
fmt.Println(<span class="hljs-string">"Price not found or wrong type"</span>)
}
title, ok := getValue[<span class="hljs-keyword">string</span>](result, <span class="hljs-string">"title"</span>) <span class="hljs-comment">// Try to get a string from the map</span>
<span class="hljs-keyword">if</span> !ok {
fmt.Println(<span class="hljs-string">"Title not found or wrong type"</span>)
}
This pattern keeps your code clean and readable while avoiding panics from unsafe assertions.
Final Thoughts
Whether you’re just starting with Go or diving into more advanced patterns like generics, understanding typecasting is key to writing safe and reliable code.
It may seem like a small detail, but incorrect type conversions can cause crashes, bugs, or silent data loss – especially when working with JSON, APIs, or user input.
Here’s what you should take away:
🧠 Always know the type you’re working with.
🔍 Use type assertions carefully and check the
ok
value.🧰 Use generics to simplify repetitive assertion logic.
💡 Don’t rely on luck — be intentional with conversions.
Mastering typecasting in Go will not only make you a better developer but also help you understand how typed systems work across different languages.
Common Type Conversion Table in Go
From Type | To Type | Syntax Example | Notes |
int | float64 | float64(myInt) | Safe, widening conversion |
float64 | int | int(myFloat) | Truncates decimals |
string | int | strconv.Atoi(myString) | Returns int and error |
int | string | strconv.Itoa(myInt) | Converts int to decimal string |
[]byte | string | string(myBytes) | Valid UTF-8 required |
string | []byte | []byte(myString) | Creates byte slice |
Helpful Packages for Type Conversion
strconv
: Converting strings to numbers and vice versareflect
: Introspect types at runtime (use with caution)encoding/json
: Automatic type mapping when unmarshalingfmt
: Quick conversion to string with formatting
References
https://go.dev/doc/effective_go
https://go.dev/doc/tutorial/generics
Golang Cast: Go Type Casting and Type Conversion
Source: freeCodeCamp Programming Tutorials: Python, JavaScript, Git & MoreÂ