Picture this scenario. It’s late. You wrote some code. But, the Golang compiler is shouting at you for using a type that it can iterate over. You have verified the type multiple times, but it still won’t compile the code for you. Why is that? Why can’t you iterate over a slice even if it is an interface{}
Here’s a code example to demonstrate this problem. You’re working with JSON data in Go, and you encounter something that seems contradictory.
// After JSON unmarshaling some unstructured data
fmt.Printf("Type: %T\n", result["users"]) // Output: []interface {}
// Let's iterate over this array of interfaces
for i, user := range result["users"] { ... } // Error: cannot range over interface{}
GoWait, what? The type is clearly []interface{}
– a slice – so why can’t we iterate over it?
The answer reveals a fundamental aspect of Go’s type system.
Why Go Uses interface{}
for JSON
Your confusion makes perfect sense – but it reveals something fundamental about how Go handles unknown data. When Go unmarshals JSON, it faces a problem: JSON is dynamically typed, but Go is statically typed.
// JSON can contain anything:
{
"name": "Alice", // string
"age": 30, // number
"active": true, // boolean
"scores": [95, 87, 92] // array
}
GoGo’s JSON package doesn’t know what types these values will be, so it uses the most flexible container possible: interface{}
. This is why result["users"]
has the static type interface{}
even though it contains a []interface{}
at runtime.
The key insight: Go’s JSON unmarshaling prioritizes flexibility over type safety when you use map[string]interface{}
.
Root of the Problem: Static vs Runtime Types in Go
The key to understanding this lies in recognizing that Go variables have two types:
- Static Type: What the compiler knows at compile time
- Runtime Type: What’s actually stored in memory at runtime
Let’s see this in action
package main
import (
"encoding/json"
"fmt"
"reflect"
)
func main() {
jsonData := `{
"users": [{"name": "Alice", "social": {"twitter": "@alice"}}]
}`
var result map[string]interface{}
json.Unmarshal([]byte(jsonData), &result)
// What we get from the map
usersFromMap := result["users"]
fmt.Printf("Static type (what compiler sees): interface{}\n")
fmt.Printf("Runtime type (what's actually stored): %T\n", usersFromMap)
fmt.Printf("Reflect type: %s\n", reflect.TypeOf(usersFromMap))
}
Go$ go run main.go
Static type (what compiler sees): interface{}
Runtime type (what's actually stored): []interface {}
Reflect type: []interface {}
BashThe Interface Philosophy Behind Your Problem
This isn’t just a JSON quirk – it’s core to how Go thinks about interfaces. In Go, interfaces describe behavior, not data structure. The empty interface interface{}
describes something with zero required behaviors – meaning it can hold absolutely anything.
var anything interface{}
anything = "hello" // Works
anything = 42 // Works
anything = []int{1,2,3} // Works - but now anything has static type interface{}
GoWhen the JSON package puts a slice into an interface{}
, the compiler only knows “this could be anything” – not “this is definitely a slice I can range over.”
Why the Go Compiler Says “No” ??!
When you declare: var result map[string]interface{}
The compiler knows that result["users"]
has the static type interface{}
.
Even though the JSON unmarshaler puts a []interface{}
value there at runtime, the compiler only sees interface{}
at compile time.
usersFromMap := result["users"] // Static type: interface{}
// This fails because the compiler sees interface{}, not []interface{}
for i, user := range usersFromMap { // ❌ Compile error
fmt.Println(i, user)
}
GoUnderstanding the Compiler’s Perspective
I see you’re trying to range over an `interface{}`. But, `interface{}` could be anything – a string, a number, a struct, or yes, even a slice. I can’t generate code to iterate over ‘anything’, so this is a compile error.
Type assertions are your way of saying to the compiler: “Trust me, I know what this really is.”
// Type assertion: interface{} → []interface{}
users, ok := result["users"].([]interface{})
// Now users has:
// - Static type: []interface{}
// - Runtime type: []interface{}
for i, user := range users { // ✅ Works perfectly!
fmt.Printf("User %d: %v\n", i, user)
}
GoHow Go’s Type Choices Affect You
Understanding why Go makes these type choices helps explain your frustration:
// What JSON unmarshaling actually produces:
var result map[string]interface{}
json.Unmarshal(data, &result)
// JSON numbers become float64 (not int!)
age := result["age"] // Static type: interface{}, Runtime type: float64
count := age.(int) // ❌ Panic! It's actually float64
count := int(age.(float64)) // ✅ Works
// This is why you need type assertions everywhere with interface{}
GoThe takeaway: map[string]interface{}
forces you into the world of runtime type checking.
Complete Type Assertion Chain in JSON Processing
When working with nested JSON structures, you often need multiple type assertions:
func processUsers(result map[string]interface{}) {
// 1st assertion: interface{} → []interface{}
users, ok := result["users"].([]interface{})
if !ok {
fmt.Println("Users field is not an array")
return
}
for i, userInterface := range users {
// 2nd assertion: interface{} → map[string]interface{}
user, ok := userInterface.(map[string]interface{})
if !ok {
fmt.Printf("User %d is not a valid object\n", i)
continue
}
fmt.Printf("Name: %v\n", user["name"])
// 3rd assertion: interface{} → map[string]interface{}
if social, ok := user["social"].(map[string]interface{}); ok {
fmt.Printf("Twitter: %v\n", social["twitter"])
}
}
}
GoWhy Each Assertion is Necessary ✅
1. 1st Assertion: Convert interface{}
to []interface{}
. So we can use range
2. 2nd Assertion: Convert each array element from interface{}
to map[string]interface{}
. So we can use bracket notation like user["name"]
3. 3rd Assertion: Convert nested objects from interface{}
to map[string]interface{}
for the same reason
When NOT to Fight the Type System
Your type assertion approach is correct for truly dynamic JSON, but recognizing when you don’t need it prevents this entire problem:
// If you know your JSON structure, skip interface{} entirely:
type Response struct {
Users []User `json:"users"`
}
type User struct {
Name string `json:"name"`
Age int `json:"age"` // JSON numbers become int, not float64!
}
var response Response
json.Unmarshal(data, &response)
// No type assertions needed - the compiler knows everything!
for _, user := range response.Users {
fmt.Printf("User: %s\n", user.Name)
}
Go💡 Most of the time, you can avoid this static/runtime type mismatch entirely by being more specific about your expected structure.
Alternative Approaches
1. Use Structs for Known Schemas
If you know your JSON structure ahead of time, define structs:
type User struct {
Name string `json:"name"`
Age int `json:"age"`
Social struct {
Twitter string `json:"twitter"`
Facebook string `json:"facebook"`
} `json:"social"`
}
type Response struct {
Users []User `json:"users"`
}
// Assuming jsonData is a []byte containing your JSON data
var response Response
json.Unmarshal(jsonData, &response)
// No type assertions needed!
for _, user := range response.Users {
fmt.Printf("Name: %s\n", user.Name)
fmt.Printf("Twitter: %s\n", user.Social.Twitter)
}
Go2. Use Libraries for Dynamic JSON
For dynamic JSON structures, consider libraries like gjson
import "github.com/tidwall/gjson"
// Assuming jsonString is a string containing your JSON data
result := gjson.Get(jsonString, "users")
result.ForEach(func(key, value gjson.Result) bool {
fmt.Printf("Name: %s\n", value.Get("name").String())
return true // continue iterating
})
GoThe Real-World Decision Tree
When you encounter JSON in Go, ask yourself:
- Do I know the structure? → Use typed structs (no type assertions needed)
- Do I know some fields? → Mix structs with
json.RawMessage
- Is it truly unknown? → Use
map[string]interface{}
+ type assertions
Your original example falls into category 3 – truly dynamic JSON where type assertions are the right solution.
Key Takeaways: Watch out for those Type Assertions
1. Static vs Runtime Types: Go variables have both compile-time and runtime types. How compile-time types actually helps in Go. More compile-time shenanigans from go.
2. Compiler Limitations: The compiler only uses static types to determine valid operations. A discussion.
3. Type Assertions: Change the static type to match the runtime type. When it fails, use https://pkg.go.dev/reflect to find the type.
4. Safety First: Always use the “comma ok” idiom to handle assertion failures.
5. Consider Alternatives: Structs for known schemas, specialized libraries for complex dynamic JSON.
TLDR: The Mental Model That Clicks
The breakthrough understanding is this: Go has two different “lenses” for looking at your data:
- Compile-time lens (static type): “I see an
interface{}
– this could be anything” - Runtime lens (actual data): “This is actually a
[]interface{}
slice”
Type assertions are your way of updating the compiler’s lens to match reality:
// Before type assertion:
usersInterface := result["users"] // Compiler sees: interface{}
for range usersInterface { ... } // ❌ "I don't know how to range over 'anything'"
// After type assertion:
users := usersInterface.([]interface{}) // Compiler now sees: []interface{}
for range users { ... } // ✅ "Ah yes, I can range over a slice"
GoThe data never changes – only what the compiler allows you to do with it.
This is why fmt.Printf("%T")
shows []interface{}
(runtime truth) while the compiler complains about interface{}
(compile-time knowledge). Your original confusion was actually perfect intuition – the types really don’t match!
Footnote:
This explanation emerged from a real debugging session where the apparent contradiction between seeing `[]interface{}` as the type but being unable to iterate over it caused significant confusion. The key insight is that `%T` shows the runtime type, while the compiler operates on static types.*
That’s it for now, live in the mix.