Skip to main content

Empty Interface and Reflection in Go

Published: May 8, 2026 Updated: August 29, 2026 Larry Qu 6 min read

any (an alias for interface{}) holds a value of any type. Reflection, through the reflect package, lets you inspect that value’s type and fields at runtime — read struct tags, call methods by name, compare unknown types, or build generic serialization logic.

Both are powerful and both are overused. Before reaching for either, check whether a typed interface or generics (Go 1.18+) would be clearer. Reflection bypasses the type system — it’s correct Go but harder to read and slower to execute than typed code.

For type assertions and type switches on interface values see Go type assertions and switches.

any / interface{}

any is just interface{} — an alias introduced in Go 1.18. An any variable can hold any value, but to do anything type-specific with it, you need a type assertion or type switch:

var v any = 42

// Type assertion — extracts the concrete value
n, ok := v.(int)
if ok {
    fmt.Println(n * 2)  // 84
}

// Type switch — handles multiple possible types
switch val := v.(type) {
case int:    fmt.Println("int:", val)
case string: fmt.Println("string:", val)
default:     fmt.Println("other:", val)
}

any is the right choice for genuinely heterogeneous collections and for functions that must accept values of truly unknown type — JSON unmarshaling into map[string]any, for example. For everything else, a typed interface or generic function is clearer and faster.

reflect.TypeOf and reflect.ValueOf

These two functions are the entry points to the reflection system:

x := 42
fmt.Println(reflect.TypeOf(x))         // int
fmt.Println(reflect.TypeOf(x).Kind())  // int (same here, differs for named types)

s := "hello"
fmt.Println(reflect.TypeOf(s))         // string
fmt.Println(reflect.TypeOf(s).Kind())  // string

type Celsius float64
c := Celsius(37.0)
fmt.Println(reflect.TypeOf(c))         // main.Celsius
fmt.Println(reflect.TypeOf(c).Kind())  // float64  ← underlying kind

Type is the Go type name (main.Celsius). Kind is the underlying kind (float64). When writing general reflection code that branches on numeric operations, check Kind, not Type.

reflect.ValueOf wraps a value in a reflect.Value that exposes methods to inspect and manipulate it:

v := reflect.ValueOf(42)
fmt.Println(v.Type())    // int
fmt.Println(v.Kind())    // int
fmt.Println(v.Int())     // 42

s := reflect.ValueOf("hello")
fmt.Println(s.String())  // hello
fmt.Println(s.Len())     // 5

Inspecting Structs

The most common practical use of reflection is reading struct fields and their tags — this is how encoding/json, GORM, and go-playground/validator work:

type User struct {
    ID    int    `json:"id"   db:"user_id"`
    Name  string `json:"name" validate:"required"`
    Email string `json:"email,omitempty"`
}

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

    // Dereference pointer if needed
    if t.Kind() == reflect.Ptr {
        t = t.Elem()
        val = val.Elem()
    }

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

    fmt.Printf("struct %s has %d fields:\n", t.Name(), t.NumField())
    for i := 0; i < t.NumField(); i++ {
        field := t.Field(i)   // reflect.StructField — type info + tags
        value := val.Field(i) // reflect.Value — the actual value

        jsonTag := field.Tag.Get("json")
        fmt.Printf("  %s (type %s) = %v [json:%q]\n",
            field.Name, field.Type, value.Interface(), jsonTag)
    }
}

u := User{ID: 1, Name: "Alice", Email: "[email protected]"}
inspectStruct(u)
// struct User has 3 fields:
//   ID (type int) = 1 [json:"id"]
//   Name (type string) = Alice [json:"name"]
//   Email (type string) = [email protected] [json:"email,omitempty"]

field.Tag.Get("json") returns the value of the json struct tag for that field. This is exactly how encoding/json decides what JSON key name to use.

Modifying Values via Reflection

Reflection can modify values, but only through a pointer. Passing a value (not pointer) to reflect.ValueOf gives you an unaddressable copy — trying to set it panics:

type Config struct {
    Host string
    Port int
}

cfg := Config{Host: "localhost", Port: 8080}

// ❌ Can't set — cfg is passed by value, fields are unaddressable
v := reflect.ValueOf(cfg)
v.FieldByName("Host").SetString("example.com")  // panics: reflect: reflect.Value.SetString using value obtained using unexported field

// ✅ Pass a pointer and call Elem() to get the addressable struct
v = reflect.ValueOf(&cfg).Elem()
field := v.FieldByName("Host")
if field.IsValid() && field.CanSet() {
    field.SetString("example.com")
}
field = v.FieldByName("Port")
if field.IsValid() && field.CanSet() {
    field.SetInt(9090)
}
fmt.Println(cfg)  // {example.com 9090}

Always check field.IsValid() (the field exists) and field.CanSet() (the field is exported and addressable) before calling any Set method. Skipping either check causes panics.

Calling Methods via Reflection

type Greeter struct{ Name string }

func (g Greeter) Hello() string {
    return "Hello, " + g.Name
}

g := Greeter{Name: "Alice"}
v := reflect.ValueOf(g)

method := v.MethodByName("Hello")
if method.IsValid() {
    results := method.Call(nil)  // nil = no arguments
    fmt.Println(results[0].String())  // "Hello, Alice"
}

// With arguments
type Math struct{}
func (m Math) Add(a, b int) int { return a + b }

m := reflect.ValueOf(Math{})
addMethod := m.MethodByName("Add")
args := []reflect.Value{reflect.ValueOf(3), reflect.ValueOf(4)}
result := addMethod.Call(args)
fmt.Println(result[0].Int())  // 7

Method reflection is how RPC frameworks, test frameworks, and plugin systems work — calling methods whose names are only known at runtime.

Reflection vs Generics

Go 1.18 added generics, which solve many problems that previously required interface{} and reflection. Generics are type-safe and typically faster:

// Before generics: required reflection or interface{} + type assertions
func containsOld(slice []interface{}, item interface{}) bool {
    for _, v := range slice {
        if reflect.DeepEqual(v, item) { return true }
    }
    return false
}

// With generics: type-safe, no reflection needed, works with any comparable type
func Contains[T comparable](slice []T, item T) bool {
    for _, v := range slice {
        if v == item { return true }
    }
    return false
}

Contains([]int{1, 2, 3}, 2)          // true
Contains([]string{"a", "b"}, "c")   // false

When to use reflection instead of generics:

  • Reading and writing struct tags — generics can’t inspect tags
  • Building serializers/deserializers for arbitrary struct types
  • Implementing ORMs or dependency injection containers
  • Plugin systems where types are truly unknown at compile time

When to prefer generics:

  • Type-flexible data structures (stacks, queues, sets)
  • Functions that work uniformly on multiple types
  • Any case where the types are constrained but flexible

Performance: Reflection Is Slow

Reflection bypasses compiler optimizations and involves interface value allocation. It’s roughly 10–50x slower than equivalent direct code:

// Direct: nanoseconds
n := myStruct.Field

// Reflection: microseconds
v := reflect.ValueOf(myStruct).FieldByName("Field").Int()

Cache reflection results when performance matters. encoding/json caches the type analysis (field list, struct tags, method lookup) in a sync.Map so parsing a given struct type only happens once per program run.

Summary

  • any (= interface{}) holds any value; type assertions and type switches extract the concrete type
  • reflect.TypeOf gives the Go type name and kind; reflect.ValueOf gives a manipulable wrapper
  • Struct inspection via t.NumField(), t.Field(i), field.Tag.Get("json") — the foundation of serialization libraries
  • Always check field.CanSet() before calling Set methods; pass a pointer and call .Elem() to get addressable fields
  • Prefer generics for type-flexible functions (Go 1.18+); use reflection only when struct tags or unknown-at-compile-time types are genuinely required
  • Cache reflection results for performance — reflection is 10–50x slower than direct field access

Resources

Comments

👍 Was this article helpful?