
Use a type assertion when you care about one concrete type (or one extra interface). Use a type switch when you branch on several types. v := i.(T) panics if i is not T. v, ok := i.(T) does not. switch v := i.(type) is the multi-way form. It is not a conversion. It peeks at the pair already inside the interface.
The failure
This looks like a conversion. It is a crash waiting for a JSON number.
package main
import "fmt"
func asInt(v any) int {
return v.(int) // panic if v is float64, json.Number, etc.
}
func main() {
fmt.Println(asInt(3.0))
}
A chain of assertions is the other smell:
if n, ok := v.(int); ok {
_ = n
} else if s, ok := v.(string); ok {
_ = s
} else if b, ok := v.(bool); ok {
_ = b
}
That is a type switch written the long way. Miss a type, forget ok, and you use a zero value as if the check succeeded.
Why they differ
An interface hides (dynamic type, dynamic value). An assertion asks: is the type exactly T (or does it satisfy interface U)? A type switch asks the same question several times, and in each case the bound variable has that case's static type.
var i any = "hi"
s := i.(string) // panic on mismatch
s, ok := i.(string) // ok == false, s == ""
switch v := i.(type) {
case string:
// v is string
case int, int64:
// v is still any: shared case
default:
// v is still any
}
Asserting i.(Person) fails if the box holds *Person. Pointer and value are different dynamic types.
Asserting to another interface (err.(Temporary)) checks the method set, not a single struct. That is how optional capabilities work (io.Closer on an io.Reader).
A nil interface fails every concrete assertion. An interface that holds (*int)(nil) succeeds i.(*int): ok is true and the pointer is nil. That is the sibling nil-interface gotcha, not an assertion bug.
The fix
One type, untrusted input, comma-ok:
func asInt(v any) (int, bool) {
n, ok := v.(int)
return n, ok
}
One type, you just put it in the box, single-result is a programmer assertion: mismatch means your code is wrong.
Several types, type switch:
func describe(v any) string {
switch x := v.(type) {
case int:
return fmt.Sprintf("int %d", x)
case string:
return fmt.Sprintf("string %q", x)
case error:
return x.Error()
default:
return fmt.Sprintf("%T", x)
}
}
Optional extra methods, assertion to an interface:
func finish(r io.Reader) {
if c, ok := r.(io.Closer); ok {
_ = c.Close()
}
}
Do not type-switch your way out of a missing interface. If every implementation needs Close, put Close on the interface you accept.
Learn more: