Type Assertions — Middle Level¶
Common Patterns¶
Pattern 1: Interface upgrade¶
Pattern 2: Custom error¶
errors.As walks the wrapped error chain.
Pattern 3: Optional method¶
type Resetter interface { Reset() }
func ResetIfPossible(x any) {
if r, ok := x.(Resetter); ok {
r.Reset()
}
}
Pattern 4: Inspect map[string]any¶
func GetString(m map[string]any, key string) (string, bool) {
v, ok := m[key]
if !ok { return "", false }
s, ok := v.(string)
return s, ok
}
Pattern 5: Type assertion to add new methods¶
type V2Reader interface {
io.Reader
NewMethod() error
}
if v2, ok := r.(V2Reader); ok {
v2.NewMethod()
}
errors.As vs Type Assertion¶
Type assertion — direct only¶
errors.As — wrapper-aware¶
errors.As: 1. Direct match: asserts to the target type 2. Unwrap chain: descends via Unwrap() 3. On success — fills target and returns true
errors.Is — sentinel comparison¶
For sentinel errors.
Type Assertion Performance¶
The compiler knows the exact type — there is no significant overhead.
Type switch is faster¶
// Multi-assertion
if s, ok := i.(string); ok { ... }
if n, ok := i.(int); ok { ... }
// Type switch
switch v := i.(type) {
case string: ...
case int: ...
}
A type switch compiles to a single jump table.
Interface to Interface Assertion¶
type Reader interface { Read([]byte) (int, error) }
type Closer interface { Close() error }
func handle(r Reader) {
if c, ok := r.(Closer); ok {
defer c.Close()
}
// ...
}
r is a Reader. If the concrete type also satisfies Closer, the assertion succeeds.
Type Assertion in Switch¶
switch v := i.(type) {
case int:
fmt.Println("int:", v)
case string:
fmt.Println("string:", v)
case nil:
fmt.Println("nil")
default:
fmt.Printf("unknown: %T\n", v)
}
Inside each case, v has the specific type. (In multi-case clauses, v keeps the any type.)
Tricky Patterns¶
Pattern: Self-modifying¶
type ModifierA struct{}
func (m *ModifierA) Apply(s *State) { ... }
type ModifierB struct{}
func (m *ModifierB) Apply(s *State) { ... }
mods := []any{&ModifierA{}, &ModifierB{}}
for _, m := range mods {
if mod, ok := m.(interface{ Apply(*State) }); ok {
mod.Apply(s)
}
}
Ad-hoc interface assertion.
Pattern: Ad-hoc capability¶
func printable(x any) {
if s, ok := x.(fmt.Stringer); ok {
fmt.Println(s.String())
} else {
fmt.Println(x)
}
}
Anti-patterns¶
1. Assertion chain¶
// Bad
if a, ok := x.(*A); ok { a.Method() }
if b, ok := x.(*B); ok { b.Method() }
if c, ok := x.(*C); ok { c.Method() }
Use a type switch or introduce an interface.
2. Single-value assertion (production)¶
Fine in tests. Use the two-value form in production.
3. Assertion in place of polymorphism¶
// Bad
func process(x any) {
if a, ok := x.(*A); ok { a.Method() }
if b, ok := x.(*B); ok { b.Method() }
}
// Good
type Processor interface { Method() }
func process(p Processor) { p.Method() }
Cheat Sheet¶
PATTERNS
─────────────────────
Interface upgrade
Custom error (errors.As)
Optional method
Map[string]any inspection
PERFORMANCE
─────────────────────
Single assertion: ~1-2 ns
Multi-assertion → type switch is faster
ERROR
─────────────────────
errors.As — wrapper-aware
errors.Is — sentinel
Direct assertion — only direct match
ANTI-PATTERNS
─────────────────────
Assertion chain → type switch
Single-value in production
Assertion in place of polymorphism
Summary¶
Middle-level type assertion topics: - Common patterns: interface upgrade, custom error, optional capability - errors.As is wrapper-aware; type assertion is direct - Performance: assertion ~1–2 ns; type switch wins for many types - Anti-patterns: assertion chains, single-value form in production, assertion replacing polymorphism