You hold a value, but you don't know what it is
You are writing a function that accepts any. The compiler stops checking types at that boundary. Inside the function, you hold a value, but you don't know what it is. Is it a string? A slice? A pointer to a struct? You need to inspect the value to decide how to handle it. Generics let you constrain types at compile time, but sometimes the type is truly unknown until the program runs. Maybe you are deserializing data from an untrusted source. Maybe you are writing a test helper that compares two values of arbitrary type. When the type is unknown, you ask the runtime for help.
Go provides the reflect package for this. The package exposes two primary entry points. reflect.TypeOf returns a reflect.Type object that describes the exact type definition. reflect.ValueOf returns a reflect.Value object that wraps the underlying data. The distinction between Type and Kind is the source of most confusion. Type is the specific identity. Kind is the underlying category.
Consider a named type. You can define type MyInt int. The Type is MyInt. The Kind is int. If you are writing a function that adds numbers, you care about the Kind. You want to handle all integer-like values. If you check the Type name, you miss MyInt. If you are writing a logger that prints type names for debugging, you care about the Type. You want to show MyInt to the developer, not just int.
Type versus Kind
reflect.TypeOf gives you the reflect.Type interface. This object knows the exact name of the type, the package path where it was defined, and the size in bytes. It also knows the Kind. reflect.ValueOf gives you a reflect.Value. This object holds the actual data. You call .Kind() on the Value to get the underlying category.
The Kind is one of the constants in the reflect package: reflect.Int, reflect.String, reflect.Slice, reflect.Ptr, and so on. There are about twenty Kinds. They map to the basic building blocks of Go types. A slice has Kind reflect.Slice. A map has Kind reflect.Map. A struct has Kind reflect.Struct. A channel has Kind reflect.Chan.
Type includes the Kind, but Type also includes the name. If you have type UserID int, the Type name is UserID and the Kind is int. If you have a plain int, the Type name is int and the Kind is int. When you write generic logic, you almost always switch on Kind. When you write diagnostics, you print the Type name.
Here is the simplest goroutine: spawn one, send a message, close the channel.
Here is the minimal reflection setup: create an interface, get the Type, get the Value, check the Kind.
package main
import (
"fmt"
"reflect"
)
func main() {
// Interface{} erases type info at compile time.
var v interface{} = 42
// TypeOf returns the reflect.Type object describing the exact type.
t := reflect.TypeOf(v)
fmt.Println("Type:", t) // int
// ValueOf returns a reflect.Value wrapping the underlying data.
val := reflect.ValueOf(v)
// Kind() reveals the underlying category, ignoring custom type names.
fmt.Println("Kind:", val.Kind()) // int
}
How the runtime represents interfaces
An interface value in Go is a pair of pointers. One pointer references a type descriptor table. The other pointer references the actual data. When you pass a value to a function accepting any, the compiler boxes the value into this pair. reflect.TypeOf extracts the type descriptor pointer and wraps it in a reflect.Type struct. reflect.ValueOf extracts the data pointer and wraps it in a reflect.Value struct.
The Kind() method reads a small integer field from the type descriptor to determine the base category. This operation is fast for a single check, but the boxing and unboxing add overhead. The compiler cannot optimize across reflection calls because the type is unknown until runtime. Every call to reflect.TypeOf or reflect.ValueOf involves runtime lookups. If you call these functions in a tight loop, the performance drops significantly. Use reflection for setup, configuration, or occasional inspection. Do not use it to process millions of items per second.
Convention aside: any is an alias for interface{} introduced in Go 1.18. The community prefers any in new code because it is shorter and signals intent. The compiler treats them identically.
Realistic example: dynamic inspection
You often need to inspect a value to handle containers differently from scalars. A common pattern is a switch on Kind. This lets you write a single function that handles slices, maps, and primitives. You use methods on reflect.Value to access the data. Len() works for slices, arrays, maps, and strings. Index() works for slices and arrays. MapKeys() works for maps.
Here is a function that prints a value, adapting to its structure.
// PrintValue inspects a value and formats it based on its kind.
func PrintValue(val any) {
if val == nil {
fmt.Println("nil")
return
}
v := reflect.ValueOf(val)
// Kind() returns the underlying category, not the named type.
switch v.Kind() {
case reflect.Slice, reflect.Array:
// Use Len() and Index() for sequential access.
fmt.Printf("Container with %d items:\n", v.Len())
for i := 0; i < v.Len(); i++ {
// Interface() converts back to a concrete value.
fmt.Printf(" [%d]: %v\n", i, v.Index(i).Interface())
}
case reflect.Map:
// Maps require MapKeys() to iterate safely.
fmt.Printf("Map with %d keys:\n", v.Len())
for _, key := range v.MapKeys() {
fmt.Printf(" %v: %v\n", key.Interface(), v.MapIndex(key).Interface())
}
default:
fmt.Printf("Scalar: %v\n", val)
}
}
The Interface() method converts a reflect.Value back to an any. This is useful when you need to pass the value to another function that expects a concrete type. You can also use v.Interface().(string) to do a type assertion directly.
Pitfalls and panics
Reflection is powerful, but it breaks the safety net of the type system. The compiler cannot catch errors in reflection code. You get panics at runtime instead.
The most dangerous input is a nil interface. If you have var x interface{} = nil, the interface pair contains two nil pointers. There is no type descriptor. There is no data. Calling reflect.ValueOf(x) panics at runtime with reflect: call of reflect.Value.Kind on zero Value. The runtime cannot wrap nothing. You must check for nil before reflecting. A nil pointer inside an interface is different. If you have var p *int = nil and pass p to a function, the interface contains a type descriptor for *int and a nil data pointer. reflect.ValueOf works. Kind() returns reflect.Ptr. You can call IsNil() to check the data pointer. The distinction matters: a nil interface has no type. A nil pointer has a type but no data.
Unexported fields present another trap. If you reflect on a struct with a private field, you can read the field's Kind and Type. You can even read the value using methods like Int() or String() if the kind supports it. However, calling Interface() on an unexported field panics with reflect: reflect.Value.Interface: cannot return value obtained from unexported field or method. The reflection package enforces Go's visibility rules. You cannot use reflection to leak private data back into the interface system. If you need to access unexported data, you must use the specific accessor methods on reflect.Value rather than converting back to any.
Another common error is passing a non-interface value to reflect.ValueOf when you expect an interface. The compiler rejects reflect.ValueOf(non-interface type nil) is invalid if you try to pass the literal nil. You must pass a typed nil or check the variable first.
Reflection also cannot modify unexported fields. If you try to set a private field using Set(), you get reflect: reflect.Value.Set using unaddressable value or a similar error depending on the context. You can only modify fields that are both exported and addressable.
Decision matrix
Use a type switch when you have a fixed set of known types and want zero allocation. Use reflect.TypeOf when you need the exact type name for logging or debugging. Use reflect.ValueOf and Kind() when you need to inspect structure dynamically, like in a serializer. Use reflect.Value.Interface() when you need to convert back to a concrete value, but only if the field is exported. Avoid reflection in hot paths: the overhead is real and the compiler cannot optimize across the boundary.
Reflection is a tool, not a crutch. Type is the name. Kind is the shape. Check nil before you reflect.