Go Reflection Deep Dive: Type, Value, and Practical Patterns
Reflection gives Go programs the ability to inspect and manipulate types and values at runtime. It is one of the most powerful features in the language, but that power comes at a cost. Before reaching for the reflect package, keep two things in mind:
- Code that uses reflection is hard to read, hard to maintain, and prone to runtime panics in production
- Performance is poor, typically one to two orders of magnitude slower than equivalent non reflective code
With that said, reflection is indispensable in certain scenarios such as serialization, ORMs, and generic frameworks. The two most important concepts in Go's reflection are Type and Value. Type is used to obtain type related information (such as the length of a Slice, the fields of a struct, or the number of parameters of a function). Value is used to get and modify the values of the original data (such as modifying elements in a slice or map, or modifying struct fields). The conversion between these and Go's primitive data types (such as int, float, map, struct, etc.) is illustrated in the diagram below:
All the methods shown in the diagram above will be covered below. We'll start with Type, then cover Value.
Throughout this post, the examples use the following types:
type People interface {
Think()
}
type User struct {
Id int `json:"id"`
Name string `json:"name"`
addr string // unexported field
Weight float64 `json:"weight"`
Height float64 `json:"height"`
}
func (u *User) BMI() float32 {
return float32(u.Weight / (u.Height * u.Height))
}
func (u User) Think() {
fmt.Printf("%s is thinking\n", u.Name)
}reflect.Type
How to Get a Type
Use TypeOf() to get a Type:
typeI := reflect.TypeOf(1)
typeS := reflect.TypeOf("hello")
fmt.Println(typeI) // int
fmt.Println(typeS) // string
typeUser := reflect.TypeOf(&User{})
fmt.Println(typeUser) // *User
fmt.Println(typeUser.Kind()) // ptr
fmt.Println(typeUser.Elem().Kind()) // structConverting Pointer Type to Non Pointer Type
typeUser := reflect.TypeOf(&User{})
typeUser2 := reflect.TypeOf(User{})
assert.IsEqual(typeUser.Elem(), typeUser2)Getting Struct Field Information
typeUser := reflect.TypeOf(User{}) // Must use struct's Type, not the pointer's Type
fieldNum := typeUser.NumField() // Number of fields
for i := 0; i < fieldNum; i++ {
field := typeUser.Field(i)
fmt.Printf("%d %s offset %d anonymous %t type %s exported %t json tag %s\n", i,
field.Name, // Field name
field.Offset, // Memory offset relative to the struct's base address; string type occupies 16 bytes
field.Anonymous, // Whether it is an anonymous (embedded) field
field.Type, // Data type, of reflect.Type
field.IsExported(), // Whether it is visible outside the package (i.e., starts with an uppercase letter)
field.Tag.Get("json")) // Get the tag defined inside `` after the field
}
fmt.Println()
// You can get a Field by name using FieldByName
if nameField, ok := typeUser.FieldByName("Name"); ok {
fmt.Printf("Name is exported %t\n", nameField.IsExported())
}
// You can also get a Field by index using FieldByIndex
thirdField := typeUser.FieldByIndex([]int{2}) // The parameter is a slice, because structs can be nested
fmt.Printf("third field name %s\n", thirdField.Name)Getting Struct Method Information
typeUser := reflect.TypeOf(User{})
methodNum := typeUser.NumMethod() // Number of methods. Methods with pointer receivers are NOT included
for i := 0; i < methodNum; i++ {
method := typeUser.Method(i)
fmt.Printf("method name:%s ,type:%s, exported:%t\n", method.Name, method.Type, method.IsExported())
}
fmt.Println()
typeUser2 := reflect.TypeOf(&User{})
methodNum = typeUser2.NumMethod() // Number of methods. Methods with BOTH pointer and value receivers are included, meaning methods implemented by the value are also implemented by the pointer (but not vice versa)
for i := 0; i < methodNum; i++ {
method := typeUser2.Method(i)
fmt.Printf("method name:%s ,type:%s, exported:%t\n", method.Name, method.Type, method.IsExported())
}Getting Function Information
func Add(a, b int) int {
return a + b
}
typeFunc := reflect.TypeOf(Add) // Get the function's type
fmt.Printf("is function type %t\n", typeFunc.Kind() == reflect.Func)
argInNum := typeFunc.NumIn() // Number of input parameters
argOutNum := typeFunc.NumOut() // Number of output parameters
for i := 0; i < argInNum; i++ {
argTyp := typeFunc.In(i)
fmt.Printf("Input parameter %d has type %s\n", i, argTyp)
}
for i := 0; i < argOutNum; i++ {
argTyp := typeFunc.Out(i)
fmt.Printf("Output parameter %d has type %s\n", i, argTyp)
}Checking if a Type Implements an Interface
// Get the interface type via reflect.TypeOf((*<interface>)(nil)).Elem(). Since People is an interface and cannot be instantiated, we cast nil to *People
typeOfPeople := reflect.TypeOf((*People)(nil)).Elem()
fmt.Printf("typeOfPeople kind is interface %t\n", typeOfPeople.Kind() == reflect.Interface)
t1 := reflect.TypeOf(User{})
t2 := reflect.TypeOf(&User{})
// If the value type implements the interface, then the pointer type also implements it; the reverse is not true
fmt.Printf("t1 implements People interface %t\n", t1.Implements(typeOfPeople))reflect.Value
How to Get a Value
Use ValueOf() to get a Value:
iValue := reflect.ValueOf(1)
sValue := reflect.ValueOf("hello")
userPtrValue := reflect.ValueOf(&User{
Id: 7,
Name: "John",
Weight: 65,
Height: 1.68,
})
fmt.Println(iValue) // 1
fmt.Println(sValue) // hello
fmt.Println(userPtrValue) // &{7 John 65 1.68}Converting Value to Type
iType := iValue.Type()
sType := sValue.Type()
userType := userPtrValue.Type()
// Calling Kind() on both the Type and the corresponding Value yields the same result
fmt.Println(iType.Kind() == reflect.Int, iValue.Kind() == reflect.Int, iType.Kind() == iValue.Kind())
fmt.Println(sType.Kind() == reflect.String, sValue.Kind() == reflect.String, sType.Kind() == sValue.Kind())
fmt.Println(userType.Kind() == reflect.Ptr, userPtrValue.Kind() == reflect.Ptr, userType.Kind() == userPtrValue.Kind())Converting Between Pointer Value and Non Pointer Value
userValue := userPtrValue.Elem() // Elem() converts a pointer Value to a non pointer Value
fmt.Println(userValue.Kind(), userPtrValue.Kind()) // struct ptr
userPtrValue3 := userValue.Addr() // Addr() converts a non pointer Value to a pointer Value
fmt.Println(userValue.Kind(), userPtrValue3.Kind()) // struct ptrGetting the Original Data from a Value
Use the Interface() function to convert a Value to interface, then use a type assertion to convert from interface to the original data type. Alternatively, call Int(), String(), etc. directly on the Value for a one step conversion.
fmt.Printf("origin value iValue is %d %d\n", iValue.Interface().(int), iValue.Int())
fmt.Printf("origin value sValue is %s %s\n", sValue.Interface().(string), sValue.String())
user := userValue.Interface().(User)
fmt.Printf("id=%d name=%s weight=%.2f height=%.2f\n", user.Id, user.Name, user.Weight, user.Height)
user2 := userPtrValue.Interface().(*User)
fmt.Printf("id=%d name=%s weight=%.2f height=%.2f\n", user2.Id, user2.Name, user2.Weight, user2.Height)Checking for Empty/Invalid Values
var i interface{} // Interface not pointing to a concrete value
v := reflect.ValueOf(i)
fmt.Printf("v holds a value: %t, type of v is Invalid: %t\n", v.IsValid(), v.Kind() == reflect.Invalid)
var user *User = nil
v = reflect.ValueOf(user) // Value points to nil
if v.IsValid() {
fmt.Printf("the value held by v is nil: %t\n", v.IsNil()) // Call IsNil() only after confirming IsValid(), otherwise it will panic
}
var u User // Only declared, all fields are zero values
v = reflect.ValueOf(u)
if v.IsValid() {
fmt.Printf("the value held by v is the zero value of its type: %t\n", v.IsZero()) // Call IsZero() only after confirming IsValid(), otherwise it will panic
}Modifying Original Data via Value
var i int = 10
var s string = "hello"
user := User{
Id: 7,
Name: "John",
Weight: 65.5,
Height: 1.68,
}
valueI := reflect.ValueOf(&i) // Since all function arguments in Go are passed by value, you must pass a pointer to modify the original value
valueS := reflect.ValueOf(&s)
valueUser := reflect.ValueOf(&user)
valueI.Elem().SetInt(8) // Since valueI corresponds to a pointer, use Elem() to get the object the pointer points to
valueS.Elem().SetString("golang")
valueUser.Elem().FieldByName("Weight").SetFloat(68.0) // FieldByName() returns a struct field by nameImportant: to modify the original data, you must pass a pointer to ValueOf. However, you cannot call Set or FieldByName on a pointer Value, so you must first convert it to a non pointer Value using Elem().
Unexported fields cannot be modified via reflection.
addrValue := valueUser.Elem().FieldByName("addr")
if addrValue.CanSet() {
addrValue.SetString("New York")
} else {
fmt.Println("addr is an unexported field and cannot be Set") // Fields starting with a lowercase letter are effectively private
}Modifying a Slice via Value
users := make([]*User, 1, 5) // len=1, cap=5
users[0] = &User{
Id: 7,
Name: "John",
Weight: 65.5,
Height: 1.68,
}
sliceValue := reflect.ValueOf(&users) // Pass the address of users since we intend to modify it via Value
if sliceValue.Elem().Len() > 0 { // Get the length of the slice
sliceValue.Elem().Index(0).Elem().FieldByName("Name").SetString("Mike")
fmt.Printf("1st user name change to %s\n", users[0].Name)
}You can even modify the slice's cap. The new cap must be between the original len and cap, meaning you can only reduce the cap.
sliceValue.Elem().SetCap(3)By increasing the len, you can effectively append elements to the slice.
sliceValue.Elem().SetLen(2)
// Use reflect.Value's Set() function to modify the underlying original data
sliceValue.Elem().Index(1).Set(reflect.ValueOf(&User{
Id: 8,
Name: "Sarah",
Weight: 80,
Height: 180,
}))
fmt.Printf("2nd user name %s\n", users[1].Name)Modifying a Map
Value.SetMapIndex(): adds a key value pair to the map
Value.MapIndex(): retrieves the value corresponding to a key from the map
u1 := &User{
Id: 7,
Name: "John",
Weight: 65.5,
Height: 1.68,
}
u2 := &User{
Id: 8,
Name: "Tom",
Weight: 65.5,
Height: 1.68,
}
userMap := make(map[int]*User, 5)
userMap[u1.Id] = u1
mapValue := reflect.ValueOf(&userMap) // Pass the address of userMap since we intend to modify it via Value
mapValue.Elem().SetMapIndex(reflect.ValueOf(u2.Id), reflect.ValueOf(u2)) // SetMapIndex adds a key value pair to the map
mapValue.Elem().MapIndex(reflect.ValueOf(u1.Id)).Elem().FieldByName("Name").SetString("Mike") // MapIndex retrieves the value for a key from the map
for k, user := range userMap {
fmt.Printf("key %d name %s\n", k, user.Name)
}Calling Functions
valueFunc := reflect.ValueOf(Add) // A function is also a data type
typeFunc := reflect.TypeOf(Add)
argNum := typeFunc.NumIn() // Number of input parameters
args := make([]reflect.Value, argNum) // Prepare the function's input arguments
for i := 0; i < argNum; i++ {
if typeFunc.In(i).Kind() == reflect.Int {
args[i] = reflect.ValueOf(3) // Assign 3 to each parameter
}
}
sumValue := valueFunc.Call(args) // Returns []reflect.Value, because Go functions can return multiple values
if typeFunc.Out(0).Kind() == reflect.Int {
sum := sumValue[0].Interface().(int) // Convert from Value to the original data type
fmt.Printf("sum=%d\n", sum)
}Calling Methods
user := User{
Id: 7,
Name: "John",
Weight: 65.5,
Height: 1.68,
}
valueUser := reflect.ValueOf(&user) // Must pass a pointer, because BMI() is defined with a pointer receiver
bmiMethod := valueUser.MethodByName("BMI") // MethodByName() returns a method by name
resultValue := bmiMethod.Call([]reflect.Value{}) // Pass an empty slice when there are no parameters
result := resultValue[0].Interface().(float32)
fmt.Printf("bmi=%.2f\n", result)
// Think() is defined with a value receiver, so valueUser can be either a pointer or a value
thinkMethod := valueUser.MethodByName("Think")
thinkMethod.Call([]reflect.Value{})
valueUser2 := reflect.ValueOf(user)
thinkMethod = valueUser2.MethodByName("Think")
thinkMethod.Call([]reflect.Value{})Creating Objects
Creating a Struct
t := reflect.TypeOf(User{})
value := reflect.New(t) // Create an object based on reflect.Type, get a pointer to it, then get the reflect.Value from the pointer
value.Elem().FieldByName("Id").SetInt(10)
user := value.Interface().(*User) // Convert the reflection type back to a Go primitive data typeCreating a Slice
var slice []User
sliceType := reflect.TypeOf(slice)
sliceValue := reflect.MakeSlice(sliceType, 1, 3)
sliceValue.Index(0).Set(reflect.ValueOf(User{
Id: 8,
Name: "Sarah",
Weight: 80,
Height: 180,
}))
users := sliceValue.Interface().([]User)
fmt.Printf("1st user name %s\n", users[0].Name)Creating a Map
var userMap map[int]*User
mapType := reflect.TypeOf(userMap)
// mapValue := reflect.MakeMap(mapType)
mapValue := reflect.MakeMapWithSize(mapType, 10)
user := &User{
Id: 7,
Name: "John",
Weight: 65.5,
Height: 1.68,
}
key := reflect.ValueOf(user.Id)
mapValue.SetMapIndex(key, reflect.ValueOf(user)) // SetMapIndex adds a key value pair to the map
mapValue.MapIndex(key).Elem().FieldByName("Name").SetString("Mike") // MapIndex retrieves the value for a key from the map
userMap = mapValue.Interface().(map[int]*User)
fmt.Printf("user name %s %s\n", userMap[7].Name, user.Name)In addition to MakeSlice() and MakeMap(), the reflect package also provides MakeChan() and MakeFunc().