Type Assertions and Reflection Boundaries
Overview
Type assertions extract concrete types from interfaces. Reflection provides runtime type inspection but should be used sparingly.
Type Assertions
var i interface{} = "hello"
s := i.(string) // Panic if wrong type
s, ok := i.(string) // Safe: ok is false if wrong type
if s, ok := i.(string); ok {
fmt.Println(s)
}Type Switches
func describe(i interface{}) {
switch v := i.(type) {
case int:
fmt.Println("int:", v*2)
case string:
fmt.Println("string:", len(v))
case bool:
fmt.Println("bool:", !v)
default:
fmt.Printf("unknown: %T\n", v)
}
}The reflect Package
import "reflect"
v := 42
t := reflect.TypeOf(v) // int
val := reflect.ValueOf(v) // 42
fmt.Println(t.Name()) // "int"
fmt.Println(t.Kind()) // int
fmt.Println(val.Int()) // 42Struct Reflection
type User struct {
Name string `json:"name"`
Age int `json:"age"`
}
u := User{Name: "Alice", Age: 30}
t := reflect.TypeOf(u)
for i := 0; i < t.NumField(); i++ {
field := t.Field(i)
fmt.Printf("%s: %s\n", field.Name, field.Tag.Get("json"))
}When to Use Reflection
✅ Good uses: - Serialization (JSON, XML) - ORM field mapping - Generic testing utilities - Dependency injection
❌ Avoid for: - Performance-critical code - Simple type checking - Normal business logic
Performance Cost
// Direct access: ~1ns
user.Name
// Reflection: ~100ns+
reflect.ValueOf(user).FieldByName("Name").String()Prefer Generics Over Reflection
// Go 1.18+: Use generics instead of reflect
func PrintSlice[T any](s []T) {
for _, v := range s {
fmt.Println(v)
}
}Summary
| Approach | Use Case |
|---|---|
| Type assertion | Extract known concrete type |
| Type switch | Handle multiple types |
| reflect | Runtime introspection (last resort) |
| Generics | Compile-time type safety |
More examples
Example: safe comma-ok assertion
Save as main.go and go run . (with go mod init example if needed).
package main
import "fmt"
func asInt(v any) {
if n, ok := v.(int); ok {
fmt.Println("int:", n)
return
}
fmt.Printf("not int: %T\n", v)
}
func main() {
asInt(7)
asInt("nope")
}Expected:
int: 7
not int: string
Example: type switch
Save as main.go and go run . (with go mod init example if needed).
package main
import "fmt"
func classify(v any) {
switch x := v.(type) {
case int:
fmt.Println("int", x*2)
case string:
fmt.Println("string len", len(x))
default:
fmt.Printf("other %T\n", x)
}
}
func main() {
classify(3)
classify("go")
classify(true)
}Expected:
int 6
string len 2
other bool
Runnable example
Save as main.go. Then:
go mod init example
go run .package main
import (
"fmt"
"reflect"
)
func describe(i any) {
switch v := i.(type) {
case int:
fmt.Println("int:", v*2)
case string:
fmt.Println("string len:", len(v))
case bool:
fmt.Println("bool not:", !v)
default:
fmt.Printf("unknown: %T\n", v)
}
}
type User struct {
Name string `json:"name"`
Age int `json:"age"`
}
func main() {
var i any = "hello"
s, ok := i.(string)
fmt.Println("assert string:", s, ok)
n, ok := i.(int)
fmt.Println("assert int:", n, ok)
describe(21)
describe("go")
describe(true)
describe(3.14)
// Reflection: useful for tags/serialization; prefer assertions/generics elsewhere.
u := User{Name: "Ada", Age: 36}
t := reflect.TypeOf(u)
for j := 0; j < t.NumField(); j++ {
f := t.Field(j)
fmt.Printf("field %s json=%q\n", f.Name, f.Tag.Get("json"))
}
}Expected output:
assert string: hello true
assert int: 0 false
int: 42
string len: 2
bool not: false
unknown: float64
field Name json="name"
field Age json="age"
What to notice: The comma-ok form never panics; a type switch is clearer than a chain of assertions. Reflection can read struct tags but costs more and loses static checking.
Try next: Replace the default branch with a generic PrintSlice[T any] for slice values, or make a failing assertion i.(int) without ok and observe the panic.