Go Reflect Package Detailed Explanation
When we encounter a new term, what we need to do is understand its definition and adapt to it as quickly as possible. Reflection is one of those terms.
First, the definition: Reflection allows a program to inspect and manipulate its structure, variables, methods, and other information at runtime. The Go language provides a reflection package (reflect
), enabling us to dynamically obtain type information and manipulate the fields and methods of objects at runtime.
Now that you know the definition, the next step is to adapt to this new term in your world and do so as quickly as possible.
How to adapt quickly? By practicing extensively and using it in practice as much as possible.
PS: The reflection example at the end of this article is a small demo using a framework internally in our company, which can automatically register HTTP methods by method names.
Main text begins:
How to Use Reflection#
In Go, every field has:
- Type
- Value
For example:
A := 10 // The type of a is int, and the value is 10, created with the help of Go's syntactic sugar.
var a int = 10 // This is how it should actually be created, specifying the type when declaring the value.
The two main types in reflect
are reflect.Type
and reflect.Value
, which allow us to obtain the Type and Value of any object.
Type#
When we want to obtain the type information of a variable, we can use the reflect.TypeOf
function. Here is an example using reflect.TypeOf
:
package main
import (
"fmt"
"reflect"
)
func main() {
var num int = 42
var str string = "Hello, Reflect!"
fmt.Println(reflect.TypeOf(num)) // Output: int
fmt.Println(reflect.TypeOf(str)) // Output: string
}
In fact, reflect
not only has Type but also Kind. You can understand Kind as the underlying type of the field. For example, in the following example:
var a myFloat64 // Custom type
var b *float64 // Pointer
reflectType(a) // Output: TypeOf: main.myFloat64, Type: myFloat64, Kind: float64
reflectType(b) // Output: TypeOf: *float64, Type: , Kind: ptr
type Person struct {
Name string
Age int
}
var p = Person{
Name: "Lixin",
Age: 21,
}
reflectType(p) // Output: TypeOf: main.Person, Type: Person, Kind: struct
You may notice that the .Name
of variables like slices, maps, and pointers returns empty; they are all considered by Go as underlying implementations or derived types of types, rather than specific named types.
For array and slice types, their names in Go are represented in expression form, such as [5]int
representing an integer array of length 5, and []string
representing a slice of strings. Since these types are defined through expressions rather than specific named types, their .Name()
method returns empty.
For map types, its name is map
and does not include specific key type and value type information. This is because map types in Go are a built-in type and can be instantiated with different key types and value types, so its .Name()
method returns empty.
For pointer types, its name is *
plus the name of the type it points to. For example, for a pointer variable of type *int
, its name is *int
. However, since pointer types themselves do not have specific names, just references to other types, their .Name()
method returns empty.
This design aims to adhere to Go's type system and syntax conventions. Through reflection, we can use the .Kind()
method to obtain the kind information of these types for further judgment and processing.
Here, you can further learn about the true definition of Kind in the reflect package:
// Go reflect standard library source code
// A Kind represents the specific kind of type that a Type represents.
// The zero Kind is not a valid kind.
type Kind uint
const (
Invalid Kind = iota
Bool
Int
Int8
Int16
Int32
Int64
Uint
Uint8
Uint16
Uint32
Uint64
Uintptr
Float32
Float64
Complex64
Complex128
Array
Chan
Func
Interface
Map
Pointer
Slice
String
Struct
UnsafePointer
)
Value#
The Value type in reflect represents the value information of each field in Go, and we can obtain the raw value information through ValueOf()
.
Here are some commonly used methods:
Interface()
: Returns a value of typeinterface{}
, representing the raw value held byreflect.Value
. It can be converted to a specific type through type assertion.Bool()
: Returns the raw value of typebool
.Int()
: Returns the raw value of typeint
.Float()
: Returns the raw value of typefloat64
.String()
: Returns the raw value of typestring
.Type()
: Returns a value of typereflect.Type
, representing the type of the value held byreflect.Value
.
In the following example, you should understand how to use it:
package main
import (
"fmt"
"reflect"
)
func main() {
// Use reflect.ValueOf to get reflect.Value
v := reflect.ValueOf(42)
// Get the raw value
value := v.Interface()
fmt.Println(value) // Output: 42
// Use type assertion to convert the raw value to a specific type
if i, ok := value.(int); ok {
fmt.Println(i * 2) // Output: 84
}
// Use other methods to get the raw value
b := reflect.ValueOf(true).Bool()
fmt.Println(b) // Output: true
f := reflect.ValueOf(3.14).Float()
fmt.Println(f) // Output: 3.14
s := reflect.ValueOf("hello").String()
fmt.Println(s) // Output: hello
t := reflect.ValueOf(42).Type()
fmt.Println(t) // Output: int
}
Changing the Underlying Value of a Variable via Reflection#
package main
import (
"fmt"
"reflect"
)
type Person struct {
Name string
Age int
}
func main() {
p := Person{Name: "Alice", Age: 25}
// Use reflection to get the pointer value of the variable
// Here it must be a pointer because we are changing the value through the underlying pointer
v := reflect.ValueOf(&p)
// Check if it is a pointer type and addressable
if v.Kind() == reflect.Ptr && v.Elem().CanSet() {
// Get the value pointed to by the pointer
elem := v.Elem()
// Get the field's value and modify it
nameField := elem.FieldByName("Name")
if nameField.IsValid() && nameField.Kind() == reflect.String {
nameField.SetString("Bob")
}
ageField := elem.FieldByName("Age")
if ageField.IsValid() && ageField.Kind() == reflect.Int {
ageField.SetInt(30)
}
}
fmt.Printf("%+v", p) // Output: {Name:Bob Age:30}
}
IsValid#
You may have noticed a method we didn't mention in the previous example: IsValid()
func (v Value) IsValid() bool
returns whether v
holds a value. If v
is the zero value of Value, it will return false; at this point, any method other than IsValid, String, or Kind will cause a panic.
func (v Value) IsNil() bool
reports whether the value held by v
is nil. The classification of the value held by v
must be one of channel, function, interface, map, pointer, or slice; otherwise, the IsNil function will cause a panic.
package main
import (
"fmt"
"reflect"
)
type Person struct {
Name string
Age int
}
func main() {
// *int type nil pointer
var a *int
fmt.Println("var a *int IsNil:", reflect.ValueOf(a).IsNil()) // var a *int IsNil: true
// nil value
fmt.Println("nil IsValid:", reflect.ValueOf(nil).IsValid()) // nil IsValid: false
// Instantiate an anonymous struct
b := struct{}{}
// Try to find the "abc" field in the struct
fmt.Println("Non-existent struct member:", reflect.ValueOf(b).FieldByName("abc").IsValid()) // Non-existent struct member: false
// Try to find the "abc" method in the struct
fmt.Println("Non-existent struct method:", reflect.ValueOf(b).MethodByName("abc").IsValid()) // Non-existent struct method: false
// map
c := map[string]int{}
// Try to find a non-existent key in the map
fmt.Println("Non-existent key in map:", reflect.ValueOf(c).MapIndex(reflect.ValueOf("娜扎")).IsValid()) // Non-existent key in map: false
}
Struct Reflection#
When we obtain type information from reflect, if the type is a struct, we can use the NumField()
and Field()
methods to obtain detailed information about the struct members, and even obtain the methods of the type through an instance of the type.
For example, in the following example:
package main
import (
"fmt"
"reflect"
)
type Person struct {
Name string
Age int
Height float64
}
func (p Person) SayHello() {
fmt.Println("Hello, my name is", p.Name)
}
func main() {
p := Person{
Name: "John",
Age: 30,
Height: 1.75,
}
// Get struct field information
t := reflect.TypeOf(p)
fmt.Println("Struct fields:")
for i := 0; i < t.NumField(); i++ {
field := t.Field(i)
fmt.Printf("Name: %s, Type: %s\n", field.Name, field.Type)
}
// Get struct field information by field name
field, ok := t.FieldByName("Age")
if ok {
fmt.Println("\nField by name:")
fmt.Printf("Name: %s, Type: %s\n", field.Name, field.Type)
}
// Get struct method information
v := reflect.ValueOf(p)
fmt.Println("\nMethods:")
for i := 0; i < t.NumMethod(); i++ {
method := t.Method(i)
fmt.Printf("Name: %s, Type: %s\n", method.Name, method.Type)
}
// Get struct method information by method name
method, ok := t.MethodByName("SayHello")
if ok {
fmt.Println("\nMethod by name:")
fmt.Printf("Name: %s, Type: %s\n", method.Name, method.Type)
}
}
// Output
Struct fields:
Name: Name, Type: string
Name: Age, Type: int
Name: Height, Type: float64
Field by name:
Name: Age, Type: int
Methods:
Name: SayHello, Type: func(main.Person)
Method by name:
Name: SayHello, Type: func(main.Person)
From the above example, you can see the power of reflect. For instance, I can automatically register HTTP methods just by writing the method name in a specific format under a struct. If I have a struct called Service, I can write Get_XXX
under this struct to register a GET service with the path /xxx
. Below is an example:
package main
import (
"fmt"
"net/http"
"reflect"
"strings"
)
type Service struct{}
func (s *Service) Get_Hubs(w http.ResponseWriter, r *http.Request) {
fmt.Println("Handling GET request for /hubs")
}
func (s *Service) Post_Name(w http.ResponseWriter, r *http.Request) {
fmt.Println("Handling POST request for /name")
}
func RegisterHTTPHandlers(service interface{}) {
svcType := reflect.TypeOf(service)
svcValue := reflect.ValueOf(service)
for i := 0; i < svcType.NumMethod(); i++ {
method := svcType.Method(i)
methodName := method.Name
if strings.HasPrefix(methodName, "Get_") || strings.HasPrefix(methodName, "Post_") {
parts := strings.Split(methodName, "_")
if len(parts) < 2 {
continue
}
path := "/" + strings.ToLower(strings.Join(parts[1:], "/"))
handler := svcValue.MethodByName(methodName).Interface().(func(http.ResponseWriter, *http.Request))
http.HandleFunc(path, func(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case http.MethodGet:
if strings.HasPrefix(methodName, "Get_") {
handler(w, r)
} else {
http.Error(w, "Method Not Allowed", http.StatusMethodNotAllowed)
}
case http.MethodPost:
if strings.HasPrefix(methodName, "Post_") {
handler(w, r)
} else {
http.Error(w, "Method Not Allowed", http.StatusMethodNotAllowed)
}
default:
http.Error(w, "Method Not Allowed", http.StatusMethodNotAllowed)
}
})
}
}
}
func main() {
service := &Service{}
RegisterHTTPHandlers(service)
fmt.Println("Listening on http://localhost:8889")
http.ListenAndServe(":8889", nil)
}
In this example, we defined a Service struct that contains two methods: Get_Hubs()
and Post_Name()
. Then, we wrote the RegisterHTTPHandlers()
function to automatically register HTTP handlers based on the method names.
This program registers the GET hubs
and POST name
methods, and you can use curl
or Postman
tools to continuously send corresponding requests to port 8889, and you will see the terminal print these.
Listening on http://localhost:8889
Handling POST request for /name
Handling GET request for /hubs
Then, think further, since we can register GET and POST request methods, can we also dynamically bind parameters, etc.? Through reflection, we can achieve all of this.
Summary#
This article introduces the use of the reflection package (reflect
) in the Go language. Reflection allows a program to inspect and manipulate its structure, variables, methods, and other information at runtime. Through reflection, we can dynamically obtain type information and manipulate the fields and methods of objects.
We first explained the definition and function of reflection and mentioned the two main types in the reflection package: reflect.Type
and reflect.Value
. The reflect.TypeOf
function is used to obtain type information of variables, while the reflect.ValueOf
function is used to obtain value information of variables.
We introduced how to use reflection to obtain type information and value information of variables. Example code demonstrated how to use reflect.TypeOf
and reflect.ValueOf
functions, and we introduced commonly used methods of reflect.Type
and reflect.Value
, such as Interface()
, Bool()
, Int()
, Float()
, and String()
.
We also mentioned the use of IsValid()
and IsNil()
methods to determine whether the reflection value is valid or nil.
Next, we introduced how to use reflection to change the underlying value of variables. Example code demonstrated how to obtain the pointer value of a variable through reflection and modify the struct field values using methods like SetString()
and SetInt()
.
Then, we detailed struct reflection. Example code showed how to use reflection to obtain field information and method information of structs, including using methods like NumField()
, Field()
, Method()
, and MethodByName()
.
Finally, I provided an application example demonstrating the functionality of automatically registering HTTP methods through reflection. By parsing the method names of a struct, methods that meet specific naming formats are automatically registered as corresponding HTTP services.
In summary, this article provides a detailed introduction to the usage of the reflection package in Go, including obtaining type information, value information, modifying variable values, and struct reflection operations. Reflection is a powerful feature that can provide flexibility and convenience in certain scenarios. However, it is important to note that since reflection uses runtime information, it incurs a certain performance cost, so it should be used cautiously in performance-sensitive scenarios.
References:
https://www.liwenzhou.com/posts/Go/reflect/ Li Wenzhou's Blog