How to Use the reflect Package in Go

Use the reflect package to inspect Go types and values at runtime for dynamic programming tasks.

How to Use the reflect Package in Go

You are building a configuration loader. You have a YAML file and a Go struct. You want to populate the struct from the file without writing a custom parser for every type. You write a function that takes the struct, but Go requires you to specify the parameter type. You can't write a function that accepts "any struct" unless you use interface{}. Once you have an interface{}, you have a box, but you don't know what is inside. You need to open the box, inspect the fields, and set values while the program runs.

This is the job of the reflect package. Reflection lets you inspect types and values at runtime. It powers encoding/json, database drivers, and dependency injection frameworks. It is the tool you reach for when you need to write code that operates on types you don't know at compile time.

Unpacking the interface

Go's type system is static. The compiler checks every type before the binary runs. Reflection provides a controlled way to bypass that check. The bridge is interface{}. An empty interface holds two words: a pointer to the type description and a pointer to the data. reflect unpacks this interface and gives you objects that let you query and manipulate the contents.

The package exposes two main entry points. reflect.TypeOf takes an interface{} and returns a reflect.Type. This object describes the type: its name, its kind, its fields, and its methods. reflect.ValueOf takes an interface{} and returns a reflect.Value. This object holds the actual data. You can read the data, modify it if it is addressable, and call methods on it.

This pattern aligns with a core Go convention: accept interfaces, return structs. Functions that use reflection accept interface{} to allow any type to pass through. They return concrete values or structs so the caller knows exactly what they have. You accept the interface to gain flexibility; you return concrete types to preserve clarity.

Minimal example

Here's the simplest reflection workflow: get the type, get the value, and read basic properties.

package main

import (
	"fmt"
	"reflect"
)

func main() {
	// A concrete value to inspect.
	name := "Alice"

	// TypeOf extracts the type description from the interface.
	t := reflect.TypeOf(name)
	// ValueOf extracts the data holder from the interface.
	v := reflect.ValueOf(name)

	// Kind returns the underlying category, like String or Int.
	// Custom types like "type ID string" still have Kind String.
	fmt.Println("Kind:", t.Kind())

	// String returns the string representation of the value.
	fmt.Println("Value:", v.String())
}

The distinction between Type and Kind matters. Type includes the full name. If you define type MyString string, the Type is MyString. Kind strips away the name and returns the underlying category: String, Int, Struct, Slice, Map, Pointer, and so on. Many reflection methods depend on Kind. You can only call .Int() on a value if its Kind is Int.

TypeOf gives the shape. ValueOf gives the substance.

Walking a struct

Real code usually involves iterating over struct fields. This is how serializers and ORMs work. You loop through fields, read their values, and often read struct tags to map data to external formats.

Here's a function that inspects a struct and prints field details, including tags.

package main

import (
	"fmt"
	"reflect"
)

type Config struct {
	Host  string
	Port  int
	Debug bool `json:"debug_mode"`
}

// InspectConfig prints field details using reflection.
func InspectConfig(c Config) {
	// ValueOf and TypeOf extract the runtime representations.
	v := reflect.ValueOf(c)
	t := reflect.TypeOf(c)

	// NumField returns the count of fields in the struct.
	for i := 0; i < v.NumField(); i++ {
		// Field gets the i-th field value and type info.
		fieldVal := v.Field(i)
		fieldType := t.Field(i)

		// Tag.Get reads the value of the named tag key.
		tag := fieldType.Tag.Get("json")

		// Interface() recovers the underlying value as an interface{}.
		fmt.Printf("%s: %v (kind: %s, tag: %s)\n",
			fieldType.Name, fieldVal.Interface(), fieldVal.Kind(), tag)
	}
}

func main() {
	cfg := Config{Host: "localhost", Port: 8080, Debug: true}
	InspectConfig(cfg)
}

Struct tags are the bridge between data and metadata. The json tag in the struct tells encoding/json how to serialize the field. Reflection reads these tags via StructField.Tag.Get. You can define your own tags for validation, database mapping, or logging. The tag string is just text; you parse it however you like.

Struct tags are the bridge between data and metadata. Read them with reflection.

Pitfalls and runtime traps

Reflection hides mistakes until runtime. The compiler cannot check reflection calls. You must handle errors and edge cases manually.

Passing a nil interface to reflect.ValueOf returns a zero reflect.Value. Calling methods on a zero value panics. The runtime stops with reflect: call of reflect.Value.Kind on zero Value. Always check v.IsValid() before using a value. A zero value is not valid.

var x interface{} = nil
v := reflect.ValueOf(x)
if !v.IsValid() {
	// Handle nil case.
}

You cannot modify a struct field via reflection unless the value is addressable. If you pass a struct by value to a function, reflection sees a copy. Changes to the copy vanish when the function returns. You need a pointer. If you try to set a field on a non-addressable value, you get reflect: reflect.Value.Set using unaddressable value. Pass pointers to ValueOf if you need to mutate.

// This panics because c is a copy.
v := reflect.ValueOf(c)
v.Field(0).SetString("new")

// This works because &c is a pointer to the original.
v := reflect.ValueOf(&c)
// Elem() dereferences the pointer to get the struct value.
v.Elem().Field(0).SetString("new")

Performance is another concern. Reflection is orders of magnitude slower than direct access. Every call to TypeOf, ValueOf, or field access involves dynamic dispatch and type checks. Don't use reflection in hot loops. If you must use reflection repeatedly on the same type, cache the reflect.Type and field indices. Computing types is work.

A zero Value is a trap. Check IsValid before you call anything.

When to use reflection

Reflection is powerful, but it comes with costs. It breaks static analysis, adds runtime overhead, and makes code harder to read. Use it only when you have no other choice.

Use reflect when you are writing framework code that must handle arbitrary types, like a serializer, a validator, or a dependency injector.

Use a type switch when you have a small, known set of types and want to handle each case explicitly without reflection overhead.

Use generics when you need type-safe operations over a constrained set of types; generics compile to efficient code and avoid reflection costs.

Use manual setters or a builder pattern when you control the types and want zero runtime overhead; explicit code is faster and easier to debug.

Avoid reflect in performance-critical paths; the dynamic dispatch and type checks add latency that adds up in tight loops.

Generics handle the common case. Reflection handles the impossible case.

Where to go next