Go: Reflection
The ability for a program to examine its own type structure is typically referred to as type introspection. In Go, the authors have provided us a way to do this in the reflect package. Let’s start taking a look at what the reflect package is, how it’s used in particular areas of the standard library, and write our own reflect code to parse out some meta data attached to our data structures.
Brief Talk: Types and Interfaces
Go is a statically typed language, meaning every variable has exactly one known type that has been established at
compile time. We’ve previously talked about interfaces, but the TLDR of the article is that interfaces
represent a contract that a concrete type can implement which are implicitly satisfied. For variables defined as
interfaces, these variables are still statically typed with the type of interfaces. For instance, the follow code block
variable r
is of a static type io.Reader
regardless of the concrete type.
var r io.Reader
r = os.Stdin
r = bufio.NewReader(r)
r = new(bytes.Buffer)
One of the interesting things about interfaces is that we can create blank interfaces, such as interface{}
or any
,
which represents the empty set of methods and is satisfied by any type at all. Recall what was stated previously, a
variable of interface type always has the same static type; however, at runtime the value stored in the interface
variable might change concrete types, that concrete type will always satisfy the interface.
Reflection
Reflection gives us a way to examine the type and value pair stored inside an interface variable. There are two
important types in the reflect
package, reflect.Type
and reflect.Value
. A reflect.Type
represents a Go type,
and reflect.Value
represents the types value. The reflect.Type
is typically referred to as a type descriptor,
which is a data structure that contains meta data about a specific type. Below is the current reflect.Type
interface
for Go version 1.23.1
.
type Type interface {
Align() int
FieldAlign() int
Method(int) Method
MethodByName(string) (Method, bool)
NumMethod() int
Name() string
PkgPath() string
Size() uintptr
String() string
Kind() Kind
Implements(u Type) bool
AssignableTo(u Type) bool
ConvertibleTo(u Type) bool
Comparable() bool
Bits() int
ChanDir() ChanDir
IsVariadic() bool
Elem() Type
Field(i int) StructField
FieldByIndex(index []int) StructField
FieldByName(name string) (StructField, bool)
FieldByNameFunc(match func(string) bool) (StructField, bool)
In(i int) Type
Key() Type
Len() int
NumField() int
NumIn() int
NumOut() int
Out(i int) Type
OverflowComplex(x complex128) bool
OverflowFloat(x float64) bool
OverflowInt(x int64) bool
OverflowUint(x uint64) bool
CanSeq() bool
CanSeq2() bool
common() *abi.Type
uncommon() *uncommonType
}
The underlying type that implements this interface is found in the internal/abi package, the structure is defined as follows:
type Type struct {
Size_ uintptr
PtrBytes uintptr // number of (prefix) bytes in the type that can contain pointers
Hash uint32 // hash of type; avoids computation in hash tables
TFlag TFlag // extra type information flags
Align_ uint8 // alignment of variable with this type
FieldAlign_ uint8 // alignment of struct field with this type
Kind_ Kind // enumeration for C
// function for comparing objects of this type
// (ptr to object A, ptr to object B) -> ==?
Equal func(unsafe.Pointer, unsafe.Pointer) bool
// GCData stores the GC type data for the garbage collector.
// If the KindGCProg bit is set in kind, GCData is a GC program.
// Otherwise it is a ptrmask bitmap. See mbitmap.go for details.
GCData *byte
Str NameOff // string form
PtrToThis TypeOff // type for pointer to this type, may be zero
}
We won’t go further than this right now, but I think it provides some pretty good insight in to what is going on
underneath the reflect
package. There are particular types that have the ability to check the application binary
interface of particular defined types in a program at runtime.
So, let’s create our own structure in a program and start taking a look at some of the ways we can use the reflect
package to find out some information about our type at runtime.
Reflection: User Defined Structures
We’ll create our own struct named Person
, initialize a new variable to be of the type Person
, and then use the
reflect
package to find out what information we can find out about our struct at runtime.
package main
import (
"fmt"
"reflect"
)
type Person struct {
Name string
Age int
}
func main() {
// create a reflect.Value from an instance of Person
s := Person{Name: "Nick", Age: 34}
v := reflect.ValueOf(s)
// access the type descriptor (reflect.Type) of the value
t := v.Type()
// Inspect the type descriptor
fmt.Println("Type Name:", t.Name()) // Person
fmt.Println("Kind:", t.Kind()) // struct
fmt.Println("Number of Fields:", t.NumField()) // 2
// inspect each field in the struct
for i := 0; i < t.NumField(); i++ {
field := t.Field(i)
fmt.Printf("Field Name: %s, Field Type: %s\n", field.Name, field.Type)
}
}
If for some reason we need to return the inverse value of reflect.ValueOf
, there is a provided reflect.Interface
method attached to the reflect.Value
that will return a static type interface{}
of the value.
package main
import (
"fmt"
"reflect"
)
type Person struct {
Name string
Age int
}
func main() {
// create a reflect.Value from an instance of MyStruct
s := Person{Name: "Nick", Age: 34}
v := reflect.ValueOf(s)
// access the type descriptor (reflect.Type) of the value
t := v.Type()
// inspect the type descriptor
fmt.Println("Type Name:", t.Name()) // Person
fmt.Println("Kind:", t.Kind()) // struct
fmt.Println("Number of Fields:", t.NumField()) // 2
// inspect each field in the struct
for i := 0; i < t.NumField(); i++ {
field := t.Field(i)
fmt.Printf("Field Name: %s, Field Type: %s\n", field.Name, field.Type)
}
// return back an interface{}
fmt.Println("interface: ", v.Interface())
}
Mutation of reflection objects is possible, but the value must be settable. If we have a struct that has an unexported field, than that field will not be settable via reflection. Let’s create an example of this.
package main
import (
"fmt"
"reflect"
)
type Person struct {
Name string
Age int
hidden bool // unexported field (not settable via reflection)
}
func main() {
// create an instance of Person
p := Person{Name: "Nick", Age: 30}
// call the reflection function to inspect and manipulate the struct
InspectAndSetStruct(&p)
// output the modified struct
fmt.Println("Modified Struct:", p)
}
func InspectAndSetStruct(s interface{}) {
// get the reflect.Value of the struct
v := reflect.ValueOf(s)
// ensure we're working with a pointer to the struct
if v.Kind() != reflect.Ptr || v.Elem().Kind() != reflect.Struct {
fmt.Println("Expected a pointer to a struct.")
return
}
// get the element that the pointer points to (the struct itself)
v = v.Elem()
// get the reflect.Type of the struct
t := v.Type()
fmt.Printf("Inspecting struct of type: %s\n", t.Name())
// iterate over the fields in the struct
for i := 0; i < t.NumField(); i++ {
field := t.Field(i)
fieldValue := v.Field(i)
fmt.Printf("Field Name: %s, Field Type: %s, Field Value: %v\n", field.Name, field.Type, fieldValue)
// check if the field is settable (exported fields only)
if fieldValue.CanSet() {
// Set new values dynamically
switch fieldValue.Kind() {
case reflect.String:
fieldValue.SetString("Updated Name")
case reflect.Int:
fieldValue.SetInt(100)
default:
fmt.Printf("Cannot set value for field: %s\n", field.Name)
}
} else {
fmt.Printf("Field %s is unexported or not settable.\n", field.Name)
}
}
}
Now, this particular use case in our example isn’t useful in general; however, we’re just trying to expose ourselves
to difference concepts that are available to use. For the last section of this article, let’s take a look at the
encoding/json
package as a way to understand how the standard library uses reflection.
Reflection: Tags
I’ve said this before, anytime that I’m looking for how to use a particular feature I’ll always start at the Go
standard library for examples. The encoding/json
package uses reflection to handle the serialization and
deserialization of Go types to and from JSON. In Go, this is referred to as marshalling and unmarshalling, which will
be accomplished by iterating through structs fields and encoding or decoding values to or from JSON fields by
evaluating meta-data that is attached as ‘Tags’. I’ll leave up the investigation of how the internals of this work for
another day.
Let’s write an example program that defines a structure with fields that contain meta-data tags then uses the reflect
package to evaluate the meta-data.
package main
import (
"fmt"
"reflect"
)
type Person struct {
Name string `json:"name" validate:"required"`
Age int `json:"age" validate:"min=18"`
Gender string `json:"gender" validate:"optional"`
}
func main() {
p := Person{Name: "Nick", Age: 30, Gender: "Male"}
ReadStructTags(p)
}
func ReadStructTags(s interface{}) {
// use reflection to get the type of the struct
v := reflect.ValueOf(s)
t := v.Type()
// ensure the passed interface is a struct
if t.Kind() != reflect.Struct {
fmt.Println("Expected a struct")
return
}
// iterate over the fields of the struct
for i := 0; i < t.NumField(); i++ {
field := t.Field(i)
fieldValue := v.Field(i)
// print field name and its value
fmt.Printf("Field Name: %s, Field Value: %v\n", field.Name, fieldValue)
// read the JSON tag
jsonTag := field.Tag.Get("json")
if jsonTag != "" {
fmt.Printf(" JSON Tag: %s\n", jsonTag)
}
// read the validation tag
validateTag := field.Tag.Get("validate")
if validateTag != "" {
fmt.Printf(" Validation Tag: %s\n", validateTag)
}
}
}
SIGABRT
What is “real”? How do you define “real”?