Go: panic: interface conversion: interface {} is string, not int

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 are float64, not int — 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

value.(int) Is the stored concrete type int? yes no You get the int value single form: PANIC interface conversion comma-ok form: v=zero, ok=false

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:

JSONGo type inside interface{}
number (30, 3.14)float64
stringstring
booleanbool
objectmap[string]interface{}
array[]interface{}
nullnil

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

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.

JSON Formatter Go: json cannot unmarshal All Error References
About the author

Pasindu Ishan is a software developer based in Sri Lanka. He builds privacy-first developer tools at JSON Dev Tools.