Reflection in Go is the mechanism by which code can inspect and manipulate values whose types aren’t known at compile time. It’s how encoding/json, fmt.Sprintf, go-playground/validator, GORM, and most Go frameworks work internally. Used correctly, it enables powerful generic behavior. Used carelessly, it produces slow, brittle code that fails at runtime instead of compile time.
This guide covers the practical patterns for reflection that appear in real libraries — struct tag inspection, dynamic field access, and building validators. For the basics of any and type assertions see Go empty interface and reflection basics.
Why Reflection Exists
Go’s type system is static — at compile time, every value has a known type. But frameworks like JSON encoders must work with any struct, not one defined at compile time. Reflection provides the runtime bridge: inspect whatever type arrives, adapt to its structure.
The cost: reflection bypasses the type system. Operations that would be compile-time errors become runtime panics. Reflection code is harder to read and typically 5–10x slower than equivalent typed code. Use it when the alternative is code generation or requiring users to implement interfaces.
Struct Tag Inspection: The Most Common Use
The most useful reflection in practice reads struct tags — key-value metadata that library authors attach to fields:
type User struct {
ID int `json:"id" db:"user_id" validate:"required"`
Name string `json:"name" db:"name" validate:"required,min=2"`
Email string `json:"email" db:"email" validate:"required,email"`
Password string `json:"password,omitempty" db:"-"`
}
func inspectTags(v any) {
t := reflect.TypeOf(v)
if t.Kind() == reflect.Ptr {
t = t.Elem()
}
if t.Kind() != reflect.Struct {
return
}
for i := 0; i < t.NumField(); i++ {
field := t.Field(i) // reflect.StructField
jsonTag := field.Tag.Get("json")
dbTag := field.Tag.Get("db")
validateTag := field.Tag.Get("validate")
fmt.Printf("%-10s json:%-20s db:%-15s validate:%s\n",
field.Name, jsonTag, dbTag, validateTag)
}
}
This is the foundation of ORM field mapping, JSON serialization, and struct validation — the framework reads your annotations once (typically cached) and uses them to drive behavior.
Building a Simple Struct Validator
A practical example: a validator that reads validate struct tags and enforces them:
func Validate(v any) []string {
t := reflect.TypeOf(v)
val := reflect.ValueOf(v)
// Dereference pointer if needed
if t.Kind() == reflect.Ptr {
t = t.Elem()
val = val.Elem()
}
if t.Kind() != reflect.Struct {
return []string{"expected struct"}
}
var errs []string
for i := 0; i < t.NumField(); i++ {
field := t.Field(i)
fieldVal := val.Field(i)
tag := field.Tag.Get("validate")
if tag == "" {
continue
}
for _, rule := range strings.Split(tag, ",") {
switch {
case rule == "required":
if fieldVal.IsZero() {
errs = append(errs, field.Name+" is required")
}
case strings.HasPrefix(rule, "min="):
min, _ := strconv.Atoi(strings.TrimPrefix(rule, "min="))
if fieldVal.Kind() == reflect.String && fieldVal.Len() < min {
errs = append(errs, fmt.Sprintf("%s must be at least %d chars", field.Name, min))
}
case rule == "email":
if s, ok := fieldVal.Interface().(string); ok {
if !strings.Contains(s, "@") {
errs = append(errs, field.Name+" is not a valid email")
}
}
}
}
}
return errs
}
// Usage
u := User{Name: "Al"} // too short, missing email
for _, err := range Validate(u) {
fmt.Println(err)
}
// ID is required
// Name must be at least 2 chars
// Email is required
In production, use github.com/go-playground/validator/v10 which implements this pattern with full Unicode support, cross-field validation, and cached reflection.
Setting Values via Reflection
Reflection can modify values, but only through an addressable pointer:
type Config struct {
Host string
Port int
Debug bool
Timeout time.Duration
}
// LoadFromEnv reads struct fields and fills them from environment variables
// based on an `env` struct tag
func LoadFromEnv(cfg any) error {
v := reflect.ValueOf(cfg)
if v.Kind() != reflect.Ptr || v.Elem().Kind() != reflect.Struct {
return fmt.Errorf("expected pointer to struct")
}
v = v.Elem()
t := v.Type()
for i := 0; i < t.NumField(); i++ {
field := t.Field(i)
envKey := field.Tag.Get("env")
if envKey == "" {
continue
}
envVal := os.Getenv(envKey)
if envVal == "" {
continue
}
fv := v.Field(i)
if !fv.CanSet() {
continue // unexported field
}
switch fv.Kind() {
case reflect.String:
fv.SetString(envVal)
case reflect.Int, reflect.Int64:
if fv.Type() == reflect.TypeOf(time.Duration(0)) {
d, err := time.ParseDuration(envVal)
if err != nil { return err }
fv.SetInt(int64(d))
} else {
n, err := strconv.ParseInt(envVal, 10, 64)
if err != nil { return err }
fv.SetInt(n)
}
case reflect.Bool:
b, err := strconv.ParseBool(envVal)
if err != nil { return err }
fv.SetBool(b)
}
}
return nil
}
// Usage
type ServerConfig struct {
Host string `env:"APP_HOST"`
Port int `env:"APP_PORT"`
Debug bool `env:"APP_DEBUG"`
Timeout time.Duration `env:"APP_TIMEOUT"`
}
cfg := &ServerConfig{Host: "localhost", Port: 8080}
LoadFromEnv(cfg)
The CanSet() check is important — calling SetString on a non-settable value panics. Fields are non-settable when: the struct was passed by value (not pointer), or the field is unexported.
Calling Methods Dynamically
Reflection can call methods by name, enabling plugin-like dispatch patterns:
type Calculator struct{}
func (c Calculator) Add(a, b int) int { return a + b }
func (c Calculator) Multiply(a, b int) int { return a * b }
func callMethod(obj any, method string, args ...any) ([]any, error) {
v := reflect.ValueOf(obj)
m := v.MethodByName(method)
if !m.IsValid() {
return nil, fmt.Errorf("method %q not found on %T", method, obj)
}
in := make([]reflect.Value, len(args))
for i, arg := range args {
in[i] = reflect.ValueOf(arg)
}
out := m.Call(in)
results := make([]any, len(out))
for i, r := range out {
results[i] = r.Interface()
}
return results, nil
}
// Usage
calc := Calculator{}
results, _ := callMethod(calc, "Add", 3, 4)
fmt.Println(results[0].(int)) // 7
This pattern is used by test frameworks, RPC systems, and dependency injection containers. The tradeoff: method name typos become runtime panics, not compile-time errors.
Caching Reflection for Performance
Reflection is 5–10x slower than direct access. For code that runs on every request (serializers, validators), cache the reflection analysis:
type structInfo struct {
fields []fieldInfo
}
type fieldInfo struct {
index int
name string
jsonTag string
validateTag string
}
var structCache sync.Map // map[reflect.Type]*structInfo
func getStructInfo(t reflect.Type) *structInfo {
if v, ok := structCache.Load(t); ok {
return v.(*structInfo)
}
info := &structInfo{}
for i := 0; i < t.NumField(); i++ {
f := t.Field(i)
info.fields = append(info.fields, fieldInfo{
index: i,
name: f.Name,
jsonTag: f.Tag.Get("json"),
validateTag: f.Tag.Get("validate"),
})
}
structCache.Store(t, info)
return info
}
encoding/json uses exactly this pattern — the first time it serializes a type, it analyzes the struct tags and caches the result. Subsequent serializations of the same type use the cached analysis.
When to Use Generics Instead
Go 1.18 generics solve many problems that previously required reflection, with full compile-time type safety:
// Before generics: reflection + runtime type assertions
func MapAny(slice []any, f func(any) any) []any { ... }
// With generics: type-safe, no reflection, faster
func Map[T, U any](slice []T, f func(T) U) []U {
result := make([]U, len(slice))
for i, v := range slice {
result[i] = f(v)
}
return result
}
// Generics: works with any comparable type, no reflection
func Contains[T comparable](slice []T, item T) bool {
for _, v := range slice {
if v == item { return true }
}
return false
}
Use generics when the types are constrained but flexible — data structures, algorithms, utility functions. Use reflection when you genuinely need to work with arbitrary structs you can’t describe with constraints — serialization formats, ORM field mapping, struct validation from tags.
Summary
- Reflection’s primary practical use: reading struct tags to implement serialization, validation, and ORM field mapping
- Always check
CanSet()before callingSetXxxon a reflect.Value — non-addressable values will panic - Cache reflection analysis (
sync.Mapkeyed byreflect.Type) for hot paths — reflection is 5–10x slower than direct access MethodByNameenables dynamic dispatch but moves errors from compile-time to runtime — document clearly- Go 1.18+ generics handle type-flexible algorithms without reflection — prefer generics for data structures and algorithms
- Use
go-playground/validator/v10for production struct validation — it implements the patterns here with full Unicode and cross-field support
Resources
- reflect package documentation
- Go Blog: The Laws of Reflection
- go-playground/validator
- Go Blog: An Introduction to Generics
Comments