Reflection is the ability for a program to inspect — and sometimes modify — its own structure at runtime. It’s how libraries like encoding/json know which fields a struct has without you telling them, and how testing libraries can compare deeply nested values.

Reflection is powerful, slow, and easy to misuse. Most Go code never touches it. But knowing how it works is essential for understanding parts of the standard library and many popular packages.

Two key types

The reflect package gives you two main types:

  • reflect.Type — describes a Go type (its name, fields, methods, etc.)
  • reflect.Value — wraps a Go value, lets you inspect (and sometimes set) it

You get them with reflect.TypeOf and reflect.ValueOf.

package main

import (
    "fmt"
    "reflect"
)

func main() {
    var x = 42
    var s = "hello"

    fmt.Println(reflect.TypeOf(x), reflect.ValueOf(x))
    fmt.Println(reflect.TypeOf(s), reflect.ValueOf(s))
}
int 42
string hello

Inspecting a struct

Where reflection earns its keep is with structs. You can list fields, read tags, and access values.

package main

import (
    "fmt"
    "reflect"
)

type User struct {
    Name  string `json:"name"`
    Email string `json:"email"`
    Age   int    `json:"age,omitempty"`
}

func main() {
    u := User{Name: "Mani", Email: "[email protected]", Age: 30}

    t := reflect.TypeOf(u)
    v := reflect.ValueOf(u)

    fmt.Println("Type:", t.Name())
    for i := 0; i < t.NumField(); i++ {
        field := t.Field(i)
        value := v.Field(i)
        jsonTag := field.Tag.Get("json")
        fmt.Printf("%s (%s) [json=%q] = %v\n", field.Name, field.Type, jsonTag, value)
    }
}
Type: User
Name (string) [json="name"] = Mani
Email (string) [json="email"] = [email protected]
Age (int) [json="age,omitempty"] = 30

Walk through it:

  • t.NumField() returns the number of fields
  • t.Field(i) returns metadata about field i (name, type, tag)
  • v.Field(i) returns the value of field i
  • field.Tag.Get("json") reads the struct tag — the \json:”…”“ annotation written next to the field

Struct tags are how reflection-based libraries — JSON encoders, ORMs, validation libraries — get configuration without you writing extra code.

A simple use case: JSON-like field listing

This pattern is the basis for libraries like encoding/json. A simplified version:

package main

import (
    "fmt"
    "reflect"
    "strings"
)

func describe(v any) {
    t := reflect.TypeOf(v)
    val := reflect.ValueOf(v)

    if t.Kind() != reflect.Struct {
        fmt.Println("not a struct")
        return
    }

    var lines []string
    for i := 0; i < t.NumField(); i++ {
        field := t.Field(i)
        value := val.Field(i)
        lines = append(lines, fmt.Sprintf("  %q: %v", field.Tag.Get("json"), value))
    }
    fmt.Println("{\n" + strings.Join(lines, ",\n") + "\n}")
}

type Product struct {
    Name  string  `json:"name"`
    Price float64 `json:"price"`
}

func main() {
    p := Product{Name: "Pen", Price: 1.25}
    describe(p)
}
{
  "name": Pen,
  "price": 1.25
}

This isn’t real JSON (we’d need to quote strings, escape characters, etc.) — but it’s the kind of work that real JSON encoders do under the hood, generalized to any struct.

Modifying values with reflection

Reflection can also set values, but with a catch: you need to pass a pointer to the value, and call Elem() to get the underlying value.

package main

import (
    "fmt"
    "reflect"
)

type Config struct {
    Host string
    Port int
}

func main() {
    c := &Config{Host: "localhost", Port: 8080}

    v := reflect.ValueOf(c).Elem()
    v.FieldByName("Host").SetString("example.com")
    v.FieldByName("Port").SetInt(443)

    fmt.Println(c.Host, c.Port)
}
example.com 443

The Elem() is the key — reflect.ValueOf(c) gives you the pointer; .Elem() follows the pointer to the actual struct, where you can modify fields.

Reflection-based modification only works on exported fields (ones with capital first letters). Trying to set an unexported field via reflection panics.

When NOT to use reflection

You should reach for reflection only when:

  • You’re writing a library that has to handle arbitrary types (JSON encoder, validator, ORM)
  • You need to inspect struct tags or method names dynamically
  • There’s no compile-time alternative — generics, interfaces — that fits

For application code, prefer interfaces and generics. Reflection is:

  • Slow — orders of magnitude slower than direct access
  • Loose — type checks happen at runtime, defeating Go’s static type system
  • Hard to readreflect.ValueOf(x).Elem().FieldByName("Y") is not friendly

A good rule: if you can solve a problem with an interface, use an interface. If you can solve it with generics, use generics. Reach for reflection only when those tools won’t fit.

Standard library uses of reflection

Knowing where reflection is used helps you understand it in context:

  • encoding/json uses reflection to read struct tags and walk fields
  • encoding/xml, encoding/gob — same idea, different formats
  • fmt uses reflection for %v and friends to format any value
  • reflect.DeepEqual uses reflection to compare nested structures
  • Popular external packages — gorm, validator, testify — all rely on reflection

What’s next

The last lesson covers generics — Go’s relatively recent feature for writing code that’s type-safe across many types. In many cases, generics are the right answer where you might have once reached for reflection.

Toggle theme (T)