🪞 Reflections in Go: What They Are and Why They Matter

In this latest blog from the developers on our Product Traction team, they explain reflections in Go, what they are and why they matter.

In Go, reflection refers to the ability of a program to inspect and manipulate its own types and values at runtime. While Go is a statically typed language designed for performance and simplicity, reflection introduces flexibility in situations where generic, dynamic behaviour is required.

Reflection is most useful when dealing with:

  • Generic functions or data structures
  • Serialization and deserialization (e.g., JSON, XML)
  • Validation and field introspection (e.g., for forms or configs)
  • Dependency injection
  • Resetting or modifying values behind interfaces and pointers

📦 The reflect Package

Go’s reflect package allows a program to manipulate objects with unknown types during runtime. This is crucial for writing code that operates across types.

import (
    "fmt"
    "reflect"
)

func inspect(i interface{}) {
    t := reflect.TypeOf(i)
    v := reflect.ValueOf(i)

    fmt.Println("Type:", t)
    fmt.Println("Kind:", t.Kind())
    fmt.Println("Value:", v)
}

func main() {
    var x float64 = 3.4
    inspect(x)
}

‍

Output:

Type: float64
Kind: float64
Value: 3.4

‍

🧰 Modifying Values at Runtime

You can modify values, but only if they are addressable and settable (typically pointers):

func modify(i interface{}) {
    v := reflect.ValueOf(i)
    if v.Kind() == reflect.Ptr && v.Elem().CanSet() {
        v.Elem().SetFloat(6.28)
    }
}

func main() {
    var x float64 = 3.4
    modify(&x)
    fmt.Println(x) // Output: 6.28
}

‍

🧱 Reflecting Over Structs

Structs are a key area where reflection really shines—especially when you want to write reusable or generic logic that works across types.

type User struct {
    ID   int
    Name string
}

func printStructFields(input interface{}) {
    v := reflect.ValueOf(input)
    t := reflect.TypeOf(input)

    if t.Kind() == reflect.Ptr {
        v = v.Elem()
        t = t.Elem()
    }

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

    for i := 0; i < v.NumField(); i++ {
        field := t.Field(i)
        value := v.Field(i)
        fmt.Printf("%s: %v\n", field.Name, value.Interface())
    }
}

func main() {
    user := &User{ID: 1, Name: "Alice"}
    printStructFields(user)
}

‍

This kind of logic is common when implementing custom loggers, serializers, ORM layers, or data validators.

🔁 Resetting a Generic Struct Behind a Pointer

What if you need to reset a generic value that’s behind a pointer? In a statically typed language like Go, you can’t just say “set this generic to zero value” if you’re working through an interface or an unknown type parameter.

Here’s how reflection can help:

func ResetValue(ptr interface{}) {
    v := reflect.ValueOf(ptr)

    if v.Kind() != reflect.Ptr || v.Elem().Kind() != reflect.Struct {
        panic("ResetValue expects a pointer to a struct")
    }

    v.Elem().Set(reflect.Zero(v.Elem().Type()))
}

And using it:

type Config struct {
    Enabled bool
    Count   int
}

func main() {
    cfg := &Config{Enabled: true, Count: 42}
    fmt.Println("Before:", *cfg)
    ResetValue(cfg)
    fmt.Println("After:", *cfg)
}

Output:

Before: {true 42}
After: {false 0}

‍

This is incredibly useful for testing, reinitializing reused memory, or generic data pipelines where types aren’t known at compile time but must be reset predictably.

🧠 Reflection + Generics

With Go 1.18+, generics reduce the need for reflection in many cases. But sometimes you still need to inspect or manipulate generic values at runtime.

func PrintGenericType[T any](val T) {
    fmt.Println("Value:", val)
    fmt.Println("Type:", reflect.TypeOf(val))
}

Great when you’re debugging or implementing advanced generic behaviour.

‍

⚠️ Use With Caution

Reflection is powerful—but with great power comes:

  • Less safety (you lose compile-time guarantees)
  • Reduced performance
  • Reduced readability

Prefer interfaces or generics where possible. Use reflection only when nothing else fits, and always isolate its usage to small, well-tested parts of your codebase.

‍


The Thin Air Labs Product Traction team provides strategic product, design and development services for companies of all sizes, with a specific focus on team extensions where they seamlessly integrate into an existing team. Whether they are deployed as a team extension or as an independent unit, they always work with a Founder-First Mindset to ensure their clients receive the support they need.

To learn more about our Product Traction service, go here.

‍

Build what's next with us