Lixv

Lixv

Go reflect Detailed Explanation

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:

  1. Type
  2. 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 type interface{}, representing the raw value held by reflect.Value. It can be converted to a specific type through type assertion.
  • Bool(): Returns the raw value of type bool.
  • Int(): Returns the raw value of type int.
  • Float(): Returns the raw value of type float64.
  • String(): Returns the raw value of type string.
  • Type(): Returns a value of type reflect.Type, representing the type of the value held by reflect.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

Loading...
Ownership of this post data is guaranteed by blockchain and smart contracts to the creator alone.