Reflection — Junior¶
1. What is reflection?¶
Reflection is the ability to look at a Go value at run time and ask questions about it — what type is it? What fields does it have? What's its current value? — and even modify it. The standard library provides this through the reflect package.
You'll meet reflection any time you use a library that "works with any type" — encoding/json, text/template, fmt, database/sql, ORM packages. They all use reflect internally.
2. Two main types: Type and Value¶
import "reflect"
x := 42
t := reflect.TypeOf(x) // reflect.Type
v := reflect.ValueOf(x) // reflect.Value
fmt.Println(t) // int
fmt.Println(v) // 42
fmt.Println(v.Kind()) // int
Typeis "what type is this?" — answers likeint,string,map[string]int.Valueis "what is the value?" — and lets you read it, modify it (with care), or call methods.
3. Reading basic values¶
v := reflect.ValueOf(42)
fmt.Println(v.Int()) // 42 (as int64)
v = reflect.ValueOf("hi")
fmt.Println(v.String()) // "hi"
v = reflect.ValueOf(3.14)
fmt.Println(v.Float()) // 3.14
If you call the wrong method for the value's kind, you get a panic:
So check v.Kind() first if you don't know the type.
4. Inspecting a struct¶
type User struct {
Name string
Age int
}
u := User{"Alice", 30}
t := reflect.TypeOf(u)
v := reflect.ValueOf(u)
for i := 0; i < t.NumField(); i++ {
f := t.Field(i)
fmt.Printf("%s = %v\n", f.Name, v.Field(i))
}
Output:
This is the foundation of every "loop over all fields of a struct" tool you've ever seen.
5. Reading struct tags¶
type Config struct {
Port int `env:"PORT" default:"8080"`
}
t := reflect.TypeOf(Config{})
f, _ := t.FieldByName("Port")
fmt.Println(f.Tag.Get("env")) // "PORT"
fmt.Println(f.Tag.Get("default")) // "8080"
Struct tags are how libraries learn extra information about your fields — JSON keys, environment variable names, validation rules. Reflection is the only way to read them at run time.
6. Modifying a value (the addressability trap)¶
x := 42
v := reflect.ValueOf(x)
v.SetInt(100) // panic: reflect: reflect.Value.SetInt using unaddressable value
You can't modify a value through reflection unless you give reflection a pointer:
x := 42
v := reflect.ValueOf(&x).Elem() // .Elem() dereferences the pointer
v.SetInt(100)
fmt.Println(x) // 100
Elem() follows the pointer and gives you a settable Value.
7. A complete tiny example: a env-var decoder¶
func loadFromEnv(target any) {
v := reflect.ValueOf(target).Elem() // settable
t := v.Type()
for i := 0; i < t.NumField(); i++ {
f := t.Field(i)
key := f.Tag.Get("env")
if key == "" { continue }
if val, ok := os.LookupEnv(key); ok {
v.Field(i).SetString(val) // assumes string field
}
}
}
type Config struct {
Host string `env:"HOST"`
}
func main() {
var c Config
loadFromEnv(&c)
fmt.Println(c.Host)
}
That tiny block is conceptually how libraries like kelseyhightower/envconfig work.
8. The big trade-off¶
Reflection is powerful and slow. A direct field access is one CPU instruction; reading the same field via v.Field(i).Int() is dozens. Code that uses reflection in a hot loop is usually 10–100× slower than the equivalent type-specific code.
| When to use reflection | When to avoid it |
|---|---|
| Library code that needs to handle "any type" | Hot paths in your application |
| One-time setup (config parsing at startup) | Per-request processing in a server |
| Test helpers | Inner loops of algorithms |
9. Common gotchas¶
| Mistake | Fix |
|---|---|
v.Set(...) panics with "unaddressable" | Reflect on &x, then call .Elem() |
| Reading a kind with the wrong method | Check v.Kind() first |
| Setting unexported field | Not allowed (without unsafe); make the field exported |
Comparing zero structs with == is fast; using reflect.DeepEqual for the same is slow | Use == whenever the type is known |
Forgetting that nil slice ≠ empty slice in DeepEqual | Use cmp.Equal for tests with controlled nil-vs-empty handling |
10. Summary¶
Reflection lets you inspect and modify values at runtime through the reflect package. The two key types are Type (what's the type?) and Value (what's the value? can I change it?). Use it for libraries that legitimately need to handle many types, and avoid it in hot paths.
Further reading¶
- "The Laws of Reflection": https://go.dev/blog/laws-of-reflection
reflectpackage docs: https://pkg.go.dev/reflect- Code-generation alternatives:
stringer,easyjson,mockery