Quick answer
A type assertion value.(int) panicked because the interface actually held a different type. The fixes:
- Use the two-value comma-ok form:
v, ok := value.(int)— no panic when it doesn't match. - Assert the type it actually holds — the message tells you (“is string, not int”).
- After
json.Unmarshal, numbers arefloat64, notint— assert.(float64). - Many possible types? Use a type switch.
The exact panic
var value interface{} = "42"
n := value.(int) // assert it's an int
// panic: interface conversion: interface {} is string, not int
goroutine 1 [running]:
main.main()
/app/main.go:8 +0x2c
An interface{} (or any interface) stores two things at runtime: a concrete type and a value of that type. A type assertion pulls the value back out under a specific type — but only if the stored type matches. When it doesn't, the single-value form panics, and the message spells out exactly what went wrong: is string, not int means the interface really held a string, but you asserted int.
How a type assertion decides to panic
The single-value assertion panics on a type mismatch; the two-value comma-ok form hands you the zero value and ok == false instead, so you stay in control.
Fix 1: the comma-ok assertion
The two-value form of a type assertion never panics. On a mismatch it returns the zero value and false:
if n, ok := value.(int); ok {
fmt.Println("it's an int:", n)
} else {
fmt.Println("not an int, got:", reflect.TypeOf(value))
}
Reach for the single-value form (n := value.(int)) only when a wrong type is genuinely a programming bug you want to crash on. For anything driven by external data, use comma-ok.
Fix 2: the JSON trap — numbers are float64
This is where the panic bites hardest, and it connects straight to working with JSON in Go. When you unmarshal into interface{} or map[string]interface{}, the encoding/json package maps JSON types to Go types with no configuration — and JSON's single number type always becomes float64:
var result map[string]interface{}
json.Unmarshal([]byte(`{"age": 30}`), &result)
age := result["age"].(int) // ❌ panic: interface {} is float64, not int
age := int(result["age"].(float64)) // ✅ assert float64, then convert
The JSON-to-Go mapping to keep in mind whenever you assert on decoded data:
| JSON | Go type inside interface{} |
|---|---|
number (30, 3.14) | float64 |
| string | string |
| boolean | bool |
| object | map[string]interface{} |
| array | []interface{} |
| null | nil |
Fix 3: unmarshal into a struct (the real fix)
Asserting types out of a generic map is fragile. If you know the shape of the JSON, decode into a typed struct and let the decoder produce real int, string, and nested types for you — no assertions, no panics:
type Person struct {
Name string `json:"name"`
Age int `json:"age"` // decoder gives you a real int
}
var p Person
if err := json.Unmarshal(data, &p); err != nil { /* handle */ }
fmt.Println(p.Age) // ✅ int, no assertion needed
If you're staring at an unfamiliar payload trying to work out its shape, format it first with the JSON Formatter to read the structure, then generate a matching struct. When you must keep numbers exact (large IDs that lose precision as float64), decode with json.NewDecoder(r) and call UseNumber() so numbers arrive as json.Number instead.
Fix 4: a type switch for heterogeneous values
When an interface value could legitimately be several types, branch on all of them with a type switch — the idiomatic, panic-free way to handle mixed JSON data:
switch v := value.(type) {
case string:
fmt.Println("string:", v)
case float64:
fmt.Println("number:", v)
case map[string]interface{}:
fmt.Println("object with", len(v), "keys")
case nil:
fmt.Println("null")
default:
fmt.Printf("unhandled type %T\n", v)
}
The nil edge case
One subtlety worth knowing: asserting a concrete type on a nil interface also panics, but the comma-ok form handles it cleanly. A nil interface holds neither a type nor a value, so any single-value assertion on it fails:
var value interface{} = nil
s := value.(string) // ❌ panic: interface conversion: interface {} is nil, not string
s, ok := value.(string) // ✅ ok == false, s == "" — no panic
This is one more reason to default to comma-ok for anything that could be absent, such as an optional field pulled from decoded JSON that may be missing entirely.
Prefer generics or a struct over interface{}
Since Go 1.18, generics remove many of the situations that used to force interface{} and its risky assertions — a function parameterized over a type keeps full type information, so there's nothing to assert and nothing to panic on. And for decoded data specifically, a typed struct (Fix 3) is almost always the better design than a map[string]interface{} you then pick apart with assertions. Treat a proliferation of type assertions as a signal that the value should have had a concrete type further upstream.
Debugging checklist
- ✓ Read the panic: “is X, not Y” — assert X, the type it really holds
- ✓ Switch to the comma-ok form
v, ok := value.(T)to stop panicking - ✓ After
json.Unmarshal, assert numbers as.(float64)thenint(...) - ✓ Better: unmarshal into a typed struct so no assertion is needed
- ✓ Value could be several types? Use
switch v := value.(type) - ✓ Need big integers exact? Decode with
UseNumber()forjson.Number
Frequently Asked Questions
What causes 'panic: interface conversion' in Go?
You performed a type assertion — value.(int) — on an interface whose stored concrete type is not the type you asserted. Go can't convert the underlying value, so it panics at runtime. The message tells you both types: ‘interface {} is string, not int’ means the interface actually held a string but you asserted int. It is always a mismatch between the real stored type and the asserted type.
How do I stop a type assertion from panicking?
Use the two-value ‘comma-ok’ form: v, ok := value.(int). When the assertion fails, ok is false and v is the zero value instead of a panic, so you can handle the mismatch gracefully. The single-value form v := value.(int) is the one that panics on a wrong type — only use it when you are certain of the type or a panic is genuinely the correct outcome.
Why does this happen after json.Unmarshal?
When you unmarshal JSON into an interface{} or a map[string]interface{}, Go decodes every JSON number as float64, every string as string, objects as map[string]interface{}, and arrays as []interface{}. So asserting result["age"].(int) panics because the value is actually a float64, not an int. Assert .(float64) and convert to int, or unmarshal into a typed struct instead of a generic map.
Why is my JSON number a float64 and not an int?
JSON has a single number type with no integer/float distinction, so Go's encoding/json decodes every number into float64 when the target is interface{}. Even a value like 42 becomes float64(42). Assert .(float64) then convert with int(f), use json.Number via a decoder with UseNumber(), or — the cleanest option — unmarshal into a struct with an int field so the decoder produces a real int for you.
When should I use a type switch instead of a single assertion?
Use a type switch when an interface value could legitimately hold several types and you want to branch on each — switch v := value.(type) { case string: ...; case float64: ...; default: ... }. It's the idiomatic way to handle heterogeneous JSON data or any interface{} whose concrete type varies at runtime, and it never panics because the default case catches everything you didn't list.
Does 'interface conversion' mean the same as a failed cast?
Go doesn't have casts between arbitrary types; it has type assertions on interfaces and explicit numeric conversions. ‘panic: interface conversion’ is specifically a failed type assertion — extracting a concrete type from an interface value that holds a different concrete type. It is not about converting an int to a float or a string to a number; those are separate conversion operations with their own rules.
Working with JSON in Go?
Format an unfamiliar payload to see its shape, or browse more Go and JSON errors.