Reflection has a reputation. If you have used languages like Java before, you have probably heard it described as powerful, advanced, and a little dangerous. Go has reflection too, and while the reflect package is not as intimidating as it first appears, it does come with rules and trade-offs that matter.
What reflection really gives you is a way for a running program to inspect itself.
What reflection means in Go
The easiest way to think about reflection is the literal meaning of the word: looking at yourself through a mirror.
In programming, reflection is how a program examines its own values at runtime.
When we write code, we already know what a variable is, what type it has, and how to operate on it. But once the program is running, it does not have a human looking over its shoulder telling it, “this value is a float64,” or “this one is a struct with two fields.” Reflection fills that gap.
It lets a value reveal what it is.
Basic reflection operations
The Go reflect package is often introduced as something complicated, but the simplest examples show it is actually quite direct.
reflect.TypeOf
If you just want to know a value’s type at runtime, start here:
func main() {
a := 1.3
fmt.Println("a的类型是", reflect.TypeOf(a))
}
Output:
a的类型是 float64
That is reflection in its most basic form. While the program is running, it can discover the type of a by asking reflect.TypeOf.
reflect.ValueOf
TypeOf gives you the type. ValueOf gives you a reflection object you can work with.
func main() {
type MyInt int
var x MyInt = 7
v := reflect.ValueOf(x)
fmt.Println(v.Type())
fmt.Println(v.Kind())
}
Output:
main.MyInt
int
There is an important distinction here:
v.Type()returns the actual declared type, heremain.MyIntv.Kind()returns the underlying kind, hereint
So reflection can show both the named type and the more fundamental category it belongs to.
Elem()
To modify a value through reflection, you usually need a pointer. Then Elem() gives you the value the pointer refers to.
func main() {
a := 1.3
v := reflect.ValueOf(&a)
elem := v.Elem()
elem.SetFloat(0.2)
fmt.Println(a)
}
Output:
0.2
Here, reflect.ValueOf(&a) gets the reflection value for the pointer, and v.Elem() gives access to the actual a. Once that reflected value is settable, SetFloat can change it.
Field()
Reflection is especially useful with structs, because you can inspect fields even when you do not know the structure in advance.
type MyData struct {
A int
b float32
}
func main() {
myData := MyData{
A: 1,
b: 1.1,
}
myDataV := reflect.ValueOf(&myData).Elem()
fmt.Println("字段a:", myDataV.Field(0))
fmt.Println("字段b:", myDataV.Field(1))
fmt.Println(myDataV)
myDataV.Field(0).SetInt(2)
fmt.Println(myDataV)
}
Output:
字段a: 1
字段b: 1.1
{1 1.1}
{2 1.1}
Even if code does not know the struct layout ahead of time, it can still access fields by index using Field().
There is also an important limitation: only fields that are accessible from outside the package can be modified. In this example, A is exported and can be changed. The lowercase b is not exported, so reflection cannot freely modify it.
Interface()
Reflection is not a one-way trip. After inspecting a reflected value, you can convert it back to a normal Go value.
func main() {
a := 1.3
v := reflect.ValueOf(a)
a1 := v.Interface().(float64)
fmt.Println(a1)
}
Output:
1.3
The reflected object can be turned back into an interface value with Interface(), and then asserted to the concrete type you need.
The three laws of reflection
Go’s reflection model is often summarized by three rules:
- Reflection goes from interface value to reflection object.
- Reflection goes from reflection object to interface value.
- To modify a reflection object, the value must be settable.
These are compact statements, but they explain nearly everything that matters in day-to-day use.
1. Reflection turns an interface value into a reflection object
In Go, interface{} can hold values of any type. The reflect package takes such values and wraps them into reflection objects like the v values shown above.
Once you have that reflection object, you can inspect the original value’s type, kind, fields, and more.
2. A reflection object can become an interface value again
This is the reverse direction. Using Interface(), a reflected value can be converted back into an ordinary Go value held in an interface.
That is exactly what happens in the earlier example:
a1 := v.Interface().(float64)
3. To modify a reflected value, it must be settable
This is where many reflection errors come from.
Consider this example again:
func main() {
a := 1.3
v := reflect.ValueOf(&a)
elem := v.Elem()
elem.SetFloat(0.2)
fmt.Println(a)
}
Why use &a instead of a directly? Because if you pass a itself, reflection works with a copy and the result is not settable. Trying to call SetFloat on that will panic.
Only when reflection has access to the original value through a pointer can it modify the data.
The same rule shows up with struct fields. If a field is unexported, external code cannot access it normally, and reflection cannot bypass that limitation in ordinary use. Attempting to modify such a field will also fail.
A look under the hood
To understand reflection more clearly, it helps to see how TypeOf and ValueOf are implemented internally.
How TypeOf works
// TypeOf returns the reflection Type that represents the dynamic type of i.
// If i is a nil interface value, TypeOf returns nil.
func TypeOf(i interface{}) Type {
eface := *(*emptyInterface)(unsafe.Pointer(&i))
return toType(eface.typ)
}
The implementation is very short. It uses unsafe.Pointer to treat the incoming interface value as an emptyInterface.
// emptyInterface is the header for an interface{} value.
type emptyInterface struct {
typ *rtype
word unsafe.Pointer
}
Then toType returns the actual reflection type:
func toType(t *rtype) Type {
if t == nil {
return nil
}
return t
}
The key idea is simple: an interface value internally carries type information, and TypeOf exposes that in the form of a reflect.Type.
How ValueOf works
// ValueOf returns a new Value initialized to the concrete value
// stored in the interface i. ValueOf(nil) returns the zero Value.
func ValueOf(i interface{}) Value {
if i == nil {
return Value{}
}
// TODO: Maybe allow contents of a Value to live on the stack.
// For now we make the contents always escape to the heap. It
// makes life easier in a few places (see chanrecv/mapassign
// comment below).
escapes(i)
return unpackEface(i)
}
The real work is done by unpackEface:
// unpackEface converts the empty interface i to a Value.
func unpackEface(i interface{}) Value {
e := (*emptyInterface)(unsafe.Pointer(&i))
// NOTE: don't read e.word until we know whether it is really a pointer or not.
t := e.typ
if t == nil {
return Value{}
}
f := flag(t.Kind())
if ifaceIndir(t) {
f |= flagIndir
}
return Value{t, e.word, f}
}
This is closely related to TypeOf, but instead of returning only type information, it packs more into a Value object.
The same emptyInterface structure appears here:
// emptyInterface is the header for an interface{} value.
type emptyInterface struct {
typ *rtype
word unsafe.Pointer
}
The word field points at the underlying data, and the resulting reflect.Value combines that pointer, the type, and some flags so the reflection system knows how the value can be used.
That is why ValueOf supports much richer operations than TypeOf.
Why reflection exists at all
At this point the practical question becomes: when would you actually use it?
A very good example is json.Marshal.
Why json.Marshal needs reflection
json.Marshal can take many different kinds of values and turn them into JSON. That sounds ordinary until you think about what it means internally.
Before runtime, the function does not know what specific type it will receive. The encoding logic must change depending on whether the input is:
- a struct
- a map
- a slice
- an array
- a pointer
- an interface
- and so on
If the input is a struct, the encoder also needs to discover what fields the struct has and what their values are.
This is exactly the sort of problem reflection solves well: inspect the incoming value dynamically, determine its type, and handle it accordingly.
How reflection shows up inside json.Marshal
A useful reading path through the standard library is:
json.Marshal -> e.marshal -> e.reflectValue -> valueEncoder -> typeEncoder -> newTypeEncoder -> newStructEncoder -> se.encode
Two points in that path show reflection’s role especially clearly.
Key point 1: newTypeEncoder
// newTypeEncoder constructs an encoderFunc for a type.
// The returned encoder only checks CanAddr when allowAddr is true.
func newTypeEncoder(t reflect.Type, allowAddr bool) encoderFunc {
...........
switch t.Kind() {
case reflect.Bool:
return boolEncoder
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
return intEncoder
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
return uintEncoder
case reflect.Float32:
return float32Encoder
case reflect.Float64:
return float64Encoder
case reflect.String:
return stringEncoder
case reflect.Interface:
return interfaceEncoder
case reflect.Struct:
return newStructEncoder(t)
case reflect.Map:
return newMapEncoder(t)
case reflect.Slice:
return newSliceEncoder(t)
case reflect.Array:
return newArrayEncoder(t)
case reflect.Ptr:
return newPtrEncoder(t)
default:
return unsupportedTypeEncoder
}
}
This is reflection doing type-based dispatch. After inspecting the value’s kind, the encoder selects a different strategy:
- if it is a
map, use a map encoder - if it is a
struct, use a struct encoder - if it is a
slice, use a slice encoder - and so on
Without reflection, a generic function like json.Marshal would not be able to adapt this way.
Key point 2: se.encode
For structs, reflection is also what exposes the fields.
func (se *structEncoder) encode(e *encodeState, v reflect.Value, opts encOpts) {
e.WriteByte('{')
first := true
for i, f := range se.fields {
fv := fieldByIndex(v, f.index)
if !fv.IsValid() || f.omitEmpty && isEmptyValue(fv) {
continue
}
if first {
first = false
} else {
e.WriteByte(',')
}
e.string(f.name, opts.escapeHTML)
e.WriteByte(':')
opts.quoted = f.quoted
se.fieldEncs[i](e, fv, opts)
}
e.WriteByte('}')
}
And here is how the field value is reached:
func fieldByIndex(v reflect.Value, index []int) reflect.Value {
for _, i := range index {
if v.Kind() == reflect.Ptr {
if v.IsNil() {
return reflect.Value{}
}
v = v.Elem()
}
v = v.Field(i)
}
return v
}
The important part is v.Field(i). That is reflection walking through the struct and extracting field values one by one for encoding.
There are many more details in the implementation, but this alone is enough to show why reflection matters: it allows generic code to inspect and process unknown values at runtime.
The cost of reflection
Reflection is useful, but it comes with two unavoidable downsides.
- It is not especially safe. Type conversions and runtime operations are easier to get wrong when the compiler cannot verify everything ahead of time.
- It is slower. Reflection adds overhead, which is one reason reflective JSON handling is often criticized for performance.
So reflection is not something to reach for casually. It is best used when you truly need runtime flexibility—situations like serialization, generic tooling, framework internals, or dynamic inspection of values whose shapes are not known in advance.