Right, so you’ve decided to play with fire. Good. Reflection in Go is exactly that: a powerful, dangerous tool that lets you reach into the guts of the language and muck about with types and values you only know about at runtime. It’s how you write code that writes code. It’s also how you create beautifully abstract, horrifyingly slow, and spectacularly panicky programs if you’re not careful.

The entire reflect package orbits around two celestial bodies: reflect.Type and reflect.Value. You cannot do anything without one or both of these. Think of reflect.Type as the blueprint of a house and reflect.Value as the actual, physical house itself, complete with all the furniture (the data) inside.

Getting a reflect.Type

A reflect.Type describes the type of something. It’s the meta-information. Is it a string? A struct? A slice of pointers to channels of a certain type? reflect.Type knows.

You get one primarily via reflect.TypeOf(), which, in a delightful bit of misdirection, doesn’t give you the type of the variable you pass in. Let’s clear that up immediately.

package main

import (
    "fmt"
    "reflect"
)

func main() {
    var number int = 42

    // This looks like it takes 'number', but look at the function signature.
    // TypeOf accepts an empty interface{}: interface{}(number)
    t := reflect.TypeOf(number)

    fmt.Println(t.String()) // "int"

    // Here's the critical part: TypeOf extracts the dynamic type *from inside the interface{}*.
    // It's not getting the type of 'number'; it's getting the type of the value *inside* the interface.
    var myString string = "hello"
    // This boxes myString into an interface{} value, and TypeOf looks inside that box.
    t2 := reflect.TypeOf(myString)
    fmt.Println(t2.String()) // "string"
}

The key insight here is that reflect.TypeOf(i interface{}) takes any value (because everything satisfies the empty interface), and it returns the dynamic type held by that interface. It’s not magic; it’s just the runtime looking inside the box you handed it.

Getting a reflect.Value

If Type is the blueprint, reflect.Value is the actual building materials and contents. It represents the actual runtime value, and it’s how you read and write to that value.

You get one via reflect.ValueOf(). Same rules apply: it takes an interface{} and gives you a Value representing what was inside.

func main() {
    var greeting string = "hello, reflection"

    // ValueOf boxes greeting and returns a Value representing the underlying string.
    v := reflect.ValueOf(greeting)

    // A Value has a Type! You can always go from Value -> Type.
    fmt.Println("Type:", v.Type()) // Type: string
    // To get the actual string back, we use the appropriate method.
    fmt.Println("Value:", v.String()) // Value: hello, reflection

    number := 3.14159
    vNum := reflect.ValueOf(number)
    fmt.Println("Value:", vNum.Float()) // Value: 3.14159
}

This seems straightforward, but here’s our first major pitfall. Try running this:

func main() {
    x := 2
    v := reflect.ValueOf(x)
    fmt.Println(v.Int()) // This is fine, it prints 2
    // v.SetInt(3)      // PANIC: panic: reflect: reflect.Value.SetInt using unaddressable value
}

Why the panic? Because we passed x by value to ValueOf(). The interface{} argument got a copy of x. Our Value object holds that copy, not the original x. Trying to SetInt on it would change the copy, which is useless and confusing, so the reflection library wisely panics to stop you. To get a Value you can set, you need to pass a pointer.

The Law of Reflection: You Need a Pointer to Change Things

This is the single most important rule. If you want to modify a value using reflection, you must start with a pointer to that value. This mirrors Go’s own semantics outside of reflection.

func main() {
    x := 2
    // Pass a pointer to x instead of the value of x.
    v := reflect.ValueOf(&x) // v is now a Value of type *int

    // We have a pointer, but we can't SetInt on a *int, we need the int itself.
    // We must "dereference" the pointer using Elem().
    // Elem() gives us the Value that the pointer points to, which we *can* set.
    p := v.Elem()
    fmt.Println(p.Int()) // 2

    // Now we can set the value.
    p.SetInt(3)
    fmt.Println(x) // 3 - the original variable has been changed!
}

Let’s break down Elem(): it’s like * for Values. If you have a Value representing a pointer, v.Elem() gives you the Value of what it points to. If you try to call Elem() on a non-pointer or nil pointer, it will panic. So the pattern is: ValueOf(&myVar).Elem() to get a settable Value.

Inspecting Complex Types

The real power comes from inspecting things you don’t already know the type of. Let’s poke at a struct.

func main() {
    type Coffee struct {
        Origin string
        Oz     int
        Iced   bool
    }
    myDrink := Coffee{Origin: "Ethiopia", Oz: 12, Iced: false}

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

    fmt.Println("Number of fields:", t.NumField()) // 3

    // Iterate through all the fields of the struct
    for i := 0; i < t.NumField(); i++ {
        fieldType := t.Field(i) // returns a StructField descriptor
        fieldValue := v.Field(i) // returns a Value of this field
        fmt.Printf("Field %d: Name=%s, Type=%v, Value=%v\n",
            i, fieldType.Name, fieldType.Type, fieldValue.Interface())
    }
    // Output:
    // Field 0: Name=Origin, Type=string, Value=Ethiopia
    // Field 1: Name=Oz, Type=int, Value=12
    // Field 2: Name=Iced, Type=bool, Value=false
}

Notice we used v.Field(i).Interface() to get the value back as a generic interface{}. This is the reverse of ValueOf(): it packs the value inside the Value object back into an interface{} so you can use it. It’s clean, but it’s also a bit slow—you’re boxing the value again.

A crucial best practice here: always check v.Field(i).CanInterface() before calling .Interface(). If a field is unexported (lowercase), this will return false, and calling .Interface() on it will panic. The reflect package protects unexported fields fiercely, and rightly so. It’s a boundary you’re not meant to cross outside of the package that defines them.