Go Advanced #4 reflect Package — Handling Types at Runtime
After #3 Generics, this time another flavor of dynamic tool. reflect.
reflect is a tool for inspecting and manipulating types at runtime. You will rarely reach for it directly, but it appears inside libraries like encoding/json, text/template, and ORMs. Understanding how it works makes those libraries make sense.
Two basic concepts — Type and Value #
import "reflect"
func main() {
x := 42
t := reflect.TypeOf(x)
v := reflect.ValueOf(x)
fmt.Println(t) // int
fmt.Println(v) // 42
fmt.Println(t.Kind()) // int
}reflect.Type— type metadata (name, kind, fields, methods, etc.)reflect.Value— the value itself plus type info
Kind — broad type category #
v := reflect.ValueOf(struct{ X int }{42})
fmt.Println(v.Kind()) // structKind is a broad category (int, string, struct, slice, map, ptr, etc.), while Type is more specific — for example, a defined type like mypkg.User.
Inspecting struct fields #
type User struct {
Name string
Age int
Email string
}
u := User{"Dokyung Lee", 30, "x@y.z"}
v := reflect.ValueOf(u)
t := v.Type()
for i := 0; i < t.NumField(); i++ {
field := t.Field(i)
value := v.Field(i)
fmt.Printf("%s %v = %v\n", field.Name, field.Type, value)
}
// Name string = Dokyung Lee
// Age int = 30
// Email string = x@y.zt.NumField(), t.Field(i), v.Field(i) — exactly what a JSON encoder does internally.
Reading struct tags #
type User struct {
Name string `json:"name"`
Email string `json:"email,omitempty"`
}
t := reflect.TypeOf(User{})
field := t.Field(0)
fmt.Println(field.Tag.Get("json")) // nameField(i).Tag.Get("json") — extracts only the json:"..." portion. encoding/json decides how to serialize using this info.
This mechanism is what lets Go libraries accomplish so much by inspecting struct definitions alone.
Modifying values — the Value must be settable #
x := 42
v := reflect.ValueOf(&x).Elem() // pointer → the value it points to
v.SetInt(100)
fmt.Println(x) // 100Two key points:
- Pass a pointer to
ValueOf, then get the pointed-to value withElem() - That value must be settable to call
SetInt,SetString, etc.
x := 42
v := reflect.ValueOf(x) // value copy
v.CanSet() // false — copy can't be modified
v = reflect.ValueOf(&x).Elem()
v.CanSet() // trueDynamic function calls #
func add(a, b int) int { return a + b }
f := reflect.ValueOf(add)
results := f.Call([]reflect.Value{
reflect.ValueOf(1),
reflect.ValueOf(2),
})
fmt.Println(results[0].Int()) // 3Call lets you invoke a function at runtime. It is used in RPC libraries and handler dispatch.
Interface conversion — dynamic version of type assertion #
var i interface{} = "hello"
v := reflect.ValueOf(i)
fmt.Println(v.Kind()) // string
fmt.Println(v.String()) // helloWhat is inside an interface{} can be inspected without a type assertion.
Typical example — mini JSON encoding #
Once you understand it, reflect’s role becomes clear. Let’s build a simplified version in a few lines.
func toMap(v any) map[string]any {
rv := reflect.ValueOf(v)
rt := rv.Type()
m := map[string]any{}
for i := 0; i < rt.NumField(); i++ {
field := rt.Field(i)
key := field.Tag.Get("json")
if key == "" {
key = field.Name
}
m[key] = rv.Field(i).Interface()
}
return m
}
type User struct {
Name string `json:"name"`
Age int
}
toMap(User{"Dokyung Lee", 30})
// map[name:Dokyung Lee Age:30]encoding/json does essentially the same — more thoroughly (zero value handling, omitempty, nested structs, slices, etc.).
Common case — automatic env variable mapping #
type Config struct {
Port int `env:"PORT"`
Host string `env:"HOST"`
}
func loadEnv(cfg any) {
v := reflect.ValueOf(cfg).Elem()
t := v.Type()
for i := 0; i < t.NumField(); i++ {
field := t.Field(i)
key := field.Tag.Get("env")
val := os.Getenv(key)
if val == "" {
continue
}
fv := v.Field(i)
switch fv.Kind() {
case reflect.String:
fv.SetString(val)
case reflect.Int:
n, _ := strconv.Atoi(val)
fv.SetInt(int64(n))
}
}
}This pattern is the core of libraries like caarlos0/env and kelseyhightower/envconfig.
Pitfall — reflect is slow #
reflect is tens of times slower than ordinary code. The reasons:
- Type checks happen at runtime
- More memory allocations
- Compiler can’t optimize
BenchmarkDirect-8 1000000000 0.3 ns/op
BenchmarkReflect-8 50000000 30 ns/opIn hot paths — consider code generation instead of reflect (next #7). Or, interface + type switch is often faster than reflect.
Pitfall — compiler help disappears #
v.SetString("hi") // if v is int → runtime panicreflect bypasses static type checking, so misuse becomes a runtime panic rather than a compile-time error. Always check Kind() first to be safe.
Pitfall — interface{}’s peculiar nil #
var p *User = nil
var i interface{} = p
i == nil // false — interface is a (type, value) pair
v := reflect.ValueOf(i)
v.IsNil() // true — Value.IsNil looks at the inner valueFor an interface to be nil, both its type and value must be nil. reflect.Value.IsNil() tells you whether the inner pointer is nil (depending on Kind).
Relation to unsafe.Pointer
#
reflect internally uses unsafe.Pointer — it is safe by design, but misuse can recreate the same risks. Generally only library authors go deep; most users stay on the surface API.
#5 unsafe and cgo covers unsafe.
When should you use it? — avoid if you can #
reflect’s principle:
- If compile-time information is enough → generics, interfaces, code generation
- For libraries that must work without knowing the type → reflect fits
Most business logic falls in the first bucket. Direct reflect usage is more common in library and framework code.
“Reflection is never clear.” — Go proverbs
Practice — fmt.Println is also a reflect user
#
The reason fmt.Printf("%v", x) can print arbitrary types is reflect. It is a representative use case in the standard library.
fmt.Sprintf("%v", User{"Dokyung Lee", 30}) // {Dokyung Lee 30}Wrap-up #
What we covered:
reflect.Type— type metadata,reflect.Value— value + typeKind— broad type category (int, struct, ptr, etc.)- Iterating struct fields/tags —
NumField,Field,Tag.Get - Modifying values — pointer →
Elem()→ check settable →Set* - Dynamic call —
reflect.Value.Call - Slow — avoid in hot paths; if hot, use code generation
- Bypasses static typing — runtime panic risk
- Prefer generics/interfaces when possible
In the next post (#5 unsafe and cgo) — two tools that step outside Go’s safe zone. Rarely used, but knowing what calls for them is worth it.