Mastering Go Reflection: Practical Patterns and Pitfalls

Struct Tags in Action

Struct tags provide metadata for fields that can be accessed at runtime via reflection. They’re commonly used by serialization libraries (e.g., json, xml) but are fully customizable.

  • field.Tag retrieves the raw tag string.
  • field.Tag.Get("key") extracts the value associated with a specific key.
  • field.Tag.Lookup("key") returns both value and existence flag—safer for optional keys.
package main

import (
	"fmt"
	"reflect"
)

type Animal struct {
	Name string `json:"name" schema:"required"`
	Kind int    `json:"type" id:"42" deprecated:"true"`
}

func main() {
	example := Animal{Name: "Luna", Kind: 3}
	t := reflect.TypeOf(example)

	for i := 0; i < t.NumField(); i++ {
		f := t.Field(i)
		fmt.Printf("Field: %s → Tag: %q\n", f.Name, f.Tag)
	}

	if f, ok := t.FieldByName("Kind"); ok {
		if jsonVal, ok := f.Tag.Lookup("json"); ok {
			fmt.Printf("JSON key: %s\n", jsonVal)
		}
		if idVal, ok := f.Tag.Lookup("id"); ok {
			fmt.Printf("ID: %s\n", idVal)
		}
	}
}

Type vs Kind: A Runtime Dsitinction

In Go’s type system, Type refers to the declared or concrete type (e.g., MyInt, map[string]int), while Kind is the underlying category recognized by the runtime—such as int, struct, ptr, or slice. This distinction is essential when writing generic-like logic over arbitrary values.

Every reflect.Kind corresponds to one of Go’s low-level memory representations. For example:

  • type MyString string has Kind == reflect.String but Type == main.MyString.
  • *int has Kind == reflect.Ptr, regardless of whether it points to int, string, or a custom struct.
package main

import (
	"fmt"
	"reflect"
)

func describe(v interface{}) {
	rv := reflect.ValueOf(v)
	rt := reflect.TypeOf(v)
	fmt.Printf("Value: %v | Type: %v | Kind: %v\n", rv, rt, rt.Kind())
}

func main() {
	describe("hello")                    // Type: string, Kind: string
	describe(int64(42))                  // Type: int64, Kind: int64
	describe(struct{ X int }{X: 1})     // Type: struct { X int }, Kind: struct
	describe([]byte("data"))            // Type: []uint8, Kind: slice
}

Reflect.Type and Reflect.Value: Complementary Views

reflect.Type describes *what* a value is—its name, methods, fields, and composition. reflect.Value represents *the value itself*, enabling reading, setting, and method invocation.

Crucially, only exported (capitalized) fields and methods are accessible through reflection. Unexported members remain invisible—evenif the struct is passed by pointer.

package main

import (
	"fmt"
	"reflect"
	"strconv"
)

func formatValue(v reflect.Value) string {
	switch v.Kind() {
	case reflect.String:
		return strconv.Quote(v.String())
	case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
		return strconv.FormatInt(v.Int(), 10)
	case reflect.Bool:
		return strconv.FormatBool(v.Bool())
	case reflect.Ptr, reflect.Slice, reflect.Map, reflect.Chan, reflect.Func:
		return fmt.Sprintf("%v (0x%x)", v.Type(), v.Pointer())
	case reflect.Struct:
		return fmt.Sprintf("%v struct", v.Type())
	default:
		return fmt.Sprintf("%v", v.Kind())
	}
}

type Config struct {
	Host string `env:"HOST"`
	Port int    `env:"PORT"`
}

func main() {
	cfg := Config{Host: "localhost", Port: 8080}
	v := reflect.ValueOf(cfg)

	fmt.Println(formatValue(v.Field(0))) // "localhost"
	fmt.Println(formatValue(v.Field(1))) // "8080"
}

Converting Back from reflect.Value

To recover the original Go value from a reflect.Value, use v.Interface(). This returns an interface{}, requiring a type assertion for safe usage:

v := reflect.ValueOf(Config{"api.example.com", 3000})
original := v.Interface().(Config) // Safe only if type is known
fmt.Printf("%+v\n", original)      // {Host:"api.example.com" Port:3000}

For dynamic scenarios, check compatibility first using v.Type().AssignableTo(expectedType).

Unwrapping Underlying Types

Go allows aliasing types (type MyInt int) and defining new structs. To bypass user-defined abstractions and inspect the foundational representation, use v.Kind() or t.Kind(). For deeper unwrapping (e.g., pointers, interfaces), chain Elem() or Underlying():

type ID int
var x ID = 99
v := reflect.ValueOf(&x).Elem()
fmt.Println(v.Kind())        // int
fmt.Println(v.Type())      // main.ID

Enumerating Fields and Methods

Reflection enables introspection of struct layout and method sets at runtime:

  • t.NumField() + t.Field(i) iterates over exported struct fields.
  • t.NumMethod() + t.Method(i) lists exported methods (not receivers).
func printLayout(t reflect.Type) {
	fmt.Printf("Type: %v (%v)\n", t, t.Kind())
	fmt.Println("Fields:")
	for i := 0; i < t.NumField(); i++ {
		f := t.Field(i)
		fmt.Printf("  %s %v `%.100s`\n", f.Name, f.Type, f.Tag)
	}
	fmt.Println("Methods:")
	for i := 0; i < t.NumMethod(); i++ {
		m := t.Method(i)
		fmt.Printf("  %s %v\n", m.Name, m.Type)
	}
}

Reading and Modifying Values Safely

Reading is straightforward: call v.Int(), v.String(), etc. Writing requires extra care:

  • The value must be addressable (e.g., passed as pointer).
  • It must be settable: v.CanSet() == true.
  • Use v.Elem() to dereference pointers before setting.
func updateValue() {
	var count int = 5
	v := reflect.ValueOf(&count).Elem()
	if v.CanSet() && v.Kind() == reflect.Int {
		v.SetInt(42)
	}
	fmt.Println(count) // 42
}

Dynamic Method Invocation

Call methods dynamically using v.MethodByName(name), then pass arguments as a slice of reflect.Value. Arguments must match the method signature exactly—including receiver kind (value vs. pointer).

type Service struct{ Name string }

func (s Service) Greet(prefix string) string {
	return prefix + ", " + s.Name
}

func main() {
	s := Service{Name: "API"}
	v := reflect.ValueOf(s)

	if m := v.MethodByName("Greet"); m.IsValid() {
		result := m.Call([]reflect.Value{reflect.ValueOf("Hello")})
		fmt.Println(result[0].String()) // "Hello, API"
	}
}

Tags: Go reflection struct-tags reflect-type reflect-value

Posted on Sat, 07 Nov 2026 16:38:42 +0000 by kaveman50