Interfaces give you flexibility by hiding concrete types behind contracts. But sometimes you need to peek inside -- to get the actual type back. AI generates type assertions frequently, especially when working with JSONWhat is json?A text format for exchanging data between systems. It uses key-value pairs and arrays, and every programming language can read and write it., generic containers, or any parameters. Your job is evaluating whether the assertionWhat is assertion?A statement in a test that checks whether a value matches what you expected, failing the test if it does not. is safe and whether the code should have used a stronger type in the first place.
Type assertions: opening the box
When you have an interface value, you can assert it holds a specific concrete type:
var i any = "hello"
s := i.(string)
fmt.Println(s) // helloThe syntax i.(Type) says: "I believe i contains a Type. Give it to me."
But if you're wrong:
var i any = 42
s := i.(string) // PANIC: interface conversion: int is not stringv := i.(Type) instead of the safe comma-ok form. Every time you see a type assertion without ok, treat it as a potential panic site. The only exception is when the assertion is genuinely impossible to fail (like right after a type switch case).The comma-ok form: the only safe way
Just like checking if a map key exists, type assertions have a safe version:
var i any = "hello"
s, ok := i.(string)
if ok {
fmt.Println("It's a string:", s)
} else {
fmt.Println("Not a string")
}The ok boolean tells you whether the assertionWhat is assertion?A statement in a test that checks whether a value matches what you expected, failing the test if it does not. succeeded. No panic, no crash.
| Form | Behavior on wrong type | When to use |
|---|---|---|
v := i.(Type) | Panics | Only when type is guaranteed (after type switch) |
v, ok := i.(Type) | Sets ok = false, v is zero value | Always prefer this |
Type switches: the elegant alternative
Checking multiple types with if/else chains gets messy. Type switches are cleaner:
func Describe(v any) string {
switch val := v.(type) {
case string:
return fmt.Sprintf("String: %s (len=%d)", val, len(val))
case int:
return fmt.Sprintf("Int: %d", val)
case bool:
return fmt.Sprintf("Bool: %t", val)
case []int:
return fmt.Sprintf("Int slice with %d elements", len(val))
default:
return fmt.Sprintf("Unknown: %T", val)
}
}Notice the special syntax: switch val := v.(type). Inside each case, val has the concrete type -- you get full type safety within the branch.
The nil interface vs typed nil trap
This is the single most common type-assertionWhat is assertion?A statement in a test that checks whether a value matches what you expected, failing the test if it does not. bug in AI-generated Go code:
type Thing struct{}
func (t *Thing) Method() string { return "working" }
var t *Thing = nil
var i any = t // i is NOT nil!
// Type assertion succeeds because the TYPE matches
thing, ok := i.(*Thing)
fmt.Println(ok) // true
fmt.Println(thing) // <nil>
// But using thing will panic
thing.Method() // panic: nil pointer dereference> if thing, ok := i.(*Thing); ok && thing != nil {
> // Now it's actually safe
> thing.Method()
> }
>Practical patterns AI generates
JSONWhat is json?A text format for exchanging data between systems. It uses key-value pairs and arrays, and every programming language can read and write it. unmarshaling
When AI unmarshals JSON into any, it generates type switches to process the result. Know what types JSON produces:
func ProcessJSON(data []byte) (string, error) {
var result any
if err := json.Unmarshal(data, &result); err != nil {
return "", err
}
switch v := result.(type) {
case map[string]any:
return fmt.Sprintf("Object with %d keys", len(v)), nil
case []any:
return fmt.Sprintf("Array with %d items", len(v)), nil
case string:
return fmt.Sprintf("String: %s", v), nil
case float64: // JSON numbers are always float64
return fmt.Sprintf("Number: %f", v), nil
case bool:
return fmt.Sprintf("Boolean: %t", v), nil
case nil:
return "null", nil
default:
return "", fmt.Errorf("unexpected type: %T", v)
}
}| JSON type | Go type after json.Unmarshal into any |
|---|---|
"hello" | string |
42 or 3.14 | float64 (always, even integers) |
true | bool |
null | nil |
{"key": "val"} | map[string]any |
[1, 2, 3] | []any |
int instead of float64. JSON numbers always unmarshal to float64 when the target is any. If you need an integer, assert float64 first, then convert: int(v).Interface checking with type assertionWhat is assertion?A statement in a test that checks whether a value matches what you expected, failing the test if it does not.
You can assert against an interface, not just a concrete type:
func FormatValue(v any) string {
// Check if it implements fmt.Stringer
if s, ok := v.(fmt.Stringer); ok {
return s.String()
}
return fmt.Sprintf("%v", v)
}This pattern is useful for checking optional capabilities -- "does this value also support string formatting?"
Safe type conversion utility
func ToString(v any) (string, bool) {
switch s := v.(type) {
case string:
return s, true
case []byte:
return string(s), true
case fmt.Stringer:
return s.String(), true
case error:
return s.Error(), true
default:
return "", false
}
}When type assertions signal a design problem
If you see AI generating lots of type assertions, step back and ask: should this code use a specific type instead of any?
| Situation | Type assertions | Better approach |
|---|---|---|
| Processing unknown JSON | Necessary | Use json.Unmarshal into a known struct |
| Generic container | Necessary (pre-generics) | Use generics ([T any]) since Go 1.18 |
| Function with 2-3 possible types | Acceptable via type switch | Consider separate functions or an interface |
Every function takes any | Code smell | Define proper interfaces or concrete types |
| Checking optional behavior | Good (assert interface) | This is idiomatic |
any parameters with type switches where generics would be cleaner. If you see a function that takes any and immediately type-switches to handle int, float64, and string uniformly, ask: "Could this be func Process[T constraints.Ordered](v T)?"Quick reference
| Pattern | Syntax | Use case |
|---|---|---|
| Unsafe assertion | v := i.(Type) | Only when type is guaranteed |
| Safe assertion | v, ok := i.(Type) | Always prefer this |
| Type switch | switch v := i.(type) | Multiple type checks |
| Interface assertion | v, ok := i.(SomeInterface) | Check optional capabilities |
| Nil-safe pointer assertion | v, ok := i.(*T); ok && v != nil | Handle nil pointers in interfaces |