Go反射的基本使用方法

TrumanWong
9/20/2024
TrumanWong

反射在Go程序中使用的不会特别多,一般会作为框架或是基础服务的一部分(例如使用json标准库序列化时就使用了反射)。本文将介绍反射的使用方法。

反射的两种基本类型

Go语言中提供了两种基本方法可以让我们构建反射的两个基本类型:

func ValueOf(i any)

func TypeOf(i any) Type

这两个函数的参数都是空接口any,内部存储了即将被反射的变量。因此,反射与接口之间存在很强的联系。可以说,不理解接口就无法深入理解反射。

可以将reflect.Value看作反射的值,reflect.Type看作反射的实际类型。其中,reflect.Type是一个接口,包含和类型有关的许多方法签名:

type Type interface {
	Align() int
	
	FieldAlign() int
	
	Method(int) Method
	
	MethodByName(string) (Method, bool)

	NumMethod() int
	...
}

reflect.Value是一个结构体,其内部包含了很多方法。可以简单地用fmt打印reflect.TypeOfreflect.ValueOf函数生成的结果。reflect.ValueOf将打印出反射内部的值,reflect.TypeOf会打印出反射的类型:

    year := 2024
    fmt.Println("type: ", reflect.TypeOf(year))		// type: int
    fmt.Println("value: ", reflect.ValueOf(year))	// value: 2024

其中,reflect.Value类型中的Type方法可以获取当前反射的类型:

// Type returns v's type.
func (v Value) Type() Type {
	if v.flag != 0 && v.flag&flagMethod == 0 {
		return (*rtype)(noescape(unsafe.Pointer(v.typ_))) // inline of toRType(v.typ()), for own inlining in inline test
	}
	return v.typeSlow()
}

因此,reflect.Value可以转换为reflect.Typereflect.Valuereflect.Type都具有Kind方法,可以获取标识类型的Kind,其底层是unit8Go语言中的内置类型都可以用唯一的整数进行标识:

type Kind uint8

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
)

如下所示,通过Kind类型可以方便地验证反射的类型是否相同:

    year := 2024
    fmt.Println(reflect.ValueOf(year).Kind() == reflect.Int) // true

反射转换为接口

reflect.Value中的Interface方法以空接口的形式返回reflect.Value中的值。如果要进一步获取空接口的真实值,可以通过接口的断言语法对接口进行转换。下例实现了从值到反射,再从反射到值的过程:

    var year = 2024
    pointer := reflect.ValueOf(&year)
    val := reflect.ValueOf(year)
    convertPointer := pointer.Interface().(*int)
    convertVal := val.Interface().(int)
    fmt.Println(*convertPointer, convertVal)

除了使用接口进行转换,reflect.Value还提供了一些转换到具体类型的方法,这些特殊的方法可以加快转换的速度。另外,这些方法经过了特殊的处理,因此不管反射内部类型是int8int16,还是int32,通过Int方法后都将转换为int64

func (v Value) String() string
func (v Value) Int() int64
func (v Value) Float() float64

下面是一个简单的示例演示这些特殊的函数:

package main

import (
	"fmt"
	"reflect"
)

func main() {
	var a = 1
	x := reflect.ValueOf(a).Int()
	fmt.Printf("type: %T, value: %v\n", x, x)
	b := "truman"
	y := reflect.ValueOf(b).String()
	fmt.Printf("type: %T, value: %v\n", y, y)
	c := 3.14
	z := reflect.ValueOf(c).Float()
	fmt.Printf("type: %T, value: %v\n", z, z)
}

输出结果:

type: int64, value: 1
type: string, value: truman
type: float64, value: 3.14
如果要转换的类型与实际类型不相符,则会在运行时报错。

下例的反射中存储的实际是int指针,如果要转换为int类型,则会报错:

package main

import (
	"fmt"
	"reflect"
)

func main() {
	a := 1
	x := reflect.ValueOf(&a).Int()
	fmt.Println(x)
}

报错信息:

panic: reflect: call of reflect.Value.Int on ptr Value

Elem()间接访问

如果反射中存储的是指针或接口,那么如何访问指针指向的数据呢?reflect.Value提供了Elem()方法返回指针或接口指向的数据:

func (v Value) Elem() Value

将前面出错的示例改为如下形式,即可正常运行:

	a := 1
	x := reflect.ValueOf(&a).Elem().Int()
	fmt.Println(x)
如果Value存储的不是指针或接口,则使用Elem方法时会出错,因此在使用时要非常小心。

如下示例:

	a := 1
	x := reflect.ValueOf(a).Elem().Int()
	fmt.Println(x)

报错信息:

panic: reflect: call of reflect.Value.Elem on int Value

当涉及修改反射的值时,Elem方法是非常必要的。我们已经知道,接口中存储的是指针,那么我们要修改的究竟是指针本身还是指针指向的数据呢?这个时候Elem方法就起到了关键作用。

为了更好地理解Elem方法的功能,下面举一个特殊的例子,反射类型是一个空接口,而空接口中包含了int类型的指针:

package main

import (
	"fmt"
	"reflect"
)

func main() {
	var a = 1
	var b = &a
	var c interface{} = b
	val := reflect.ValueOf(&c)
	aElem := val.Elem()
	fmt.Println(aElem.Kind())
	bElem := aElem.Elem()
	fmt.Println(bElem.Kind())
	cElem := bElem.Elem()
	fmt.Println(cElem.Kind())
}

通过三次Elem方法,打印出的返回值类型分别为:

interface
ptr
int

接下来还会看到,在修改反射值时也需要使用到Elem方法。

reflect.Type类型仍然有Elem方法,但是该方法只用于获取类型。该方法不仅仅可以返回指针和接口指向的类型,还可以返回数组、通道、切片、指针、哈希表存储的类型。

修改反射的值

有多种方式可以修改反射中存储的值,例如reflect.ValueSet方法:

func (v Value) Set(x Value)

该方法的参数仍然是reflect.Value但是要求反射中的类型必须是指针

只有当反射中存储的实际值是指针时才能赋值,否则是没有意义的,因为在反射之前,实际值被转换为了空接口,如果空接口中存储的值是一个副本,那么修改它会引起混淆,因此Go语言禁止这样做。

这与禁止用值类型去调用指针接收者的方法的原理是一样的。为了避免这种错误,reflect.value提供了CanSet方法用于获取当前的反射值是否可以赋值。

示例如下:

    a := 3.14
	val := reflect.ValueOf(a)
	fmt.Println("CanSet of val", val.CanSet())
	pointer := reflect.ValueOf(&a)
	elem := pointer.Elem()
	fmt.Println("CanSet of elem", elem.CanSet())
	elem.SetFloat(6.28)
	fmt.Println(a)

输出结果:

CanSet of val false
CanSet of elem true
6.28

结构体与反射

应用反射的大部分情况都涉及结构体。下面构造一个User结构体:

type User struct {
	ID   int
	Name string
	Age  uint
}

func (u User) ReflectCallFunc() {
	fmt.Println("ReflectCallFunc")
}

通过反射的两种基本方法将结构体转换为反射类型,用fmt简单打印出类型与值:

	user := User{
		ID:   1,
		Name: "truman",
		Age:  29,
	}
	userType := reflect.TypeOf(user)
	fmt.Println("Type Name:", userType.Name())
	userVal := reflect.ValueOf(user)
	fmt.Println("all fields:", userVal)

输出结果:

Type Name: User
all fields: {1 truman 29}

遍历结构体字段

遍历获取结构体中字段的名字及方法,可以采取下例所示的方法:

	user := User{
		ID:   1,
		Name: "truman",
		Age:  29,
	}
	userType := reflect.TypeOf(user)
	userVal := reflect.ValueOf(user)

	for i := 0; i < userType.NumField(); i++ {
		field := userType.Field(i)
		value := userVal.Field(i).Interface()
		fmt.Printf("%s:\t%s = %v\n", field.Name, field.Type, value)
	}

通过reflect.Type类型的NumField函数获取结构体中字段的个数。relect.Typereflect.Value都有Field方法,relect.TypeField方法主要用于获取结构体的元信息,其返回StructField结构,该结构包含字段名、所在包名、Tag名等基础信息:

type StructField struct {
	// Name is the field name.
	Name string

	// PkgPath is the package path that qualifies a lower case (unexported)
	// field name. It is empty for upper case (exported) field names.
	// See https://golang.org/ref/spec#Uniqueness_of_identifiers
	PkgPath string

	Type      Type      // field type
	Tag       StructTag // field tag string
	Offset    uintptr   // offset within struct, in bytes
	Index     []int     // index sequence for Type.FieldByIndex
	Anonymous bool      // is an embedded field
}

reflect.ValueField方法主要返回结构体字段的值类型,后续可以使用它修改结构体字段的值。

修改结构体字段

要修改结构体字段,可以使用reflect.Value提供的Set方法。初学者可能选择使用如下方式进行赋值操作,但这种方式是错误的:

	var s struct {
		X int
		y float64
	}
	sVal := reflect.ValueOf(s)
	xVal := sVal.Field(0)
	xVal.Set(reflect.ValueOf(100))

报错信息:

panic: reflect: reflect.Value.Set using unaddressable value

错误的原因是前面介绍Elem方法时提到的,由于reflect.ValueOf函数的参数是空接口,如果将值类型复制到空接口会产生一次复制,那么值就不是原来的值了,因此Go语言禁止了这种容易带来混淆的写法。要想修改原始值,需要在构造反射时传递结构体指针。

	sVal := reflect.ValueOf(&s)

但是只修改为指针还不够,因为Field方法中调用的方法必须为结构体

	if v.kind() != Struct {
		panic(&ValueError{"reflect.Value.Field", v.kind()})
	}

因此,需要先通过Elem方法获取指针指向的结构体值类型,才能调用Field方法。正确的使用方式如下所示。同时要注意,私有成员变量y是不能被赋值的:

	sVal := reflect.ValueOf(&s).Elem()
	xVal := sVal.Field(0)
	xVal.Set(reflect.ValueOf(100))

嵌套结构体的赋值

如下所示,User结构体中包含了CreditCard,通过下面的方式可以修改嵌套结构体中的字段:

type User struct {
	ID         int
	Name       string
	Age        uint
	CreditCard CreditCard
}

type CreditCard struct {
	Number string
}
	elem := reflect.ValueOf(&User{}).Elem()
	creditCard := elem.Field(3)
	creditCard.Set(reflect.ValueOf(CreditCard{Number: "1234567890"}))

结构体方法与动态调用

要获取任意类型对应的方法,可以使用reflect.Type提供的Method方法,Method方法需要传递方法的index序号:

func (t *rtype) Method(i int) (m Method)

如果index序号超出了范围,则会在运行时报错。该方法在大部分时候如下例所示,用于遍历反射结构体的方法:

	userType := reflect.TypeOf(&User{})
	for i := 0; i < userType.NumMethod(); i++ {
		method := userType.Method(i)
		fmt.Println(method.Name)
	}

更多时候我们使用reflect.ValueMethodByName方法,参数为方法名并返回代表该方法的reflect.Value对象。如果该方法不存在,则会返回空。

如下所示,通过Type方法将reflect.Value转换为reflect.Typereflect.Type接口中有一系列方法可以获取函数的参数个数、返回值个数、方法个数等属性:

func (u User) ReflectCallFunc(age int, name string) error {
	fmt.Println("ReflectCallFunc")
	return nil
}

func main() {
	user := User{
		ID:   1,
		Name: "Truman",
		Age:  29,
	}
	val := reflect.ValueOf(user)
	funcType := val.MethodByName("ReflectCallFunc").Type()
	fmt.Printf("numIn: %d, numOut: %d, numMethod: %d\n", funcType.NumIn(), funcType.NumOut(), funcType.NumMethod())
}

输出结果:

numIn: 2, numOut: 1, numMethod: 0

获取代表方法的reflectv.Value对象后,可以通过call方法在运行时调用方法

func (v Value) Call(in []Value) []Value

无参数调用

Call方法的参数为实际方法中传入参数的reflect.Value切片。因此,对于无参数的调用,可以传一个长度为0的切片或nil,如下所示:

	userVal := reflect.ValueOf(user)
	method := userVal.MethodByName("ReflectCallFuncNoArgs")
	method.Call(nil)
	method.Call([]reflect.Value{})

带参数调用

对于有参数的调用,需要先构造出reflect.Value类型的参数切片:

	userVal := reflect.ValueOf(user)
	method := userVal.MethodByName("ReflectCallFunc")
	method.Call([]reflect.Value{reflect.ValueOf(29), reflect.ValueOf("Truman")})

如果参数是一个指针类型,那么只需要构造指针类型的reflect.Value即可:

func (u User) ReflectCallFuncPointer(age *int, name string) {
	fmt.Println("ReflectCallFuncPointer")
}

func main() {
	userVal := reflect.ValueOf(User{})
	method := userVal.MethodByName("ReflectCallFuncPointer")
	age := 29
	method.Call([]reflect.Value{reflect.ValueOf(&age), reflect.ValueOf("Truman")})
}

和接口一样,如果方法是指针接收者,那么反射动态调用者的类型也必须是指针:

func (u *User) RefPointMethod() {
	fmt.Println("RefPointMethod")
}

func main() {
	userVal := reflect.ValueOf(User{})
	method := userVal.MethodByName("RefPointMethod")
	method.Call(nil)
}

否则如上例所示,运行时会报错:

panic: reflect: call of reflect.Value.Call on zero Value
应该将userVal := reflect.ValueOf(User{})修改为userVal := reflect.ValueOf(&User{})的指针形式。

对于方法有返回值的情况,Call方法会返回reflect.Value切片。获取返回值的反射类型后,通过将返回值转换为空接口即可进行下一步操作:

func (u User) PointMethodReturn(name string, age int) (string, int) {
	return name, age
}

func main() {
	userVal := reflect.ValueOf(&User{})
	method := userVal.MethodByName("PointMethodReturn")
	args := []reflect.Value{reflect.ValueOf("test"), reflect.ValueOf(18)}
	res := method.Call(args)
	fmt.Printf("name: %s, age: %d\n", res[0].String(), res[1].Int())
}

反射在运行时创建结构体

除了使用reflect.TypeOf函数生成已知类型的反射类型,还可以使用reflect标准库中的ArrayOfSliceOf等函数生成一些在编译时完全不存在的类型或对象。对于结构体,需要使用reflect.StructOf函数在运行时生成特定的结构体对象:

func StructOf(fields []StructField) Type

reflect.StructOf函数参数是StructField的切片,StructField代表结构体中的字段。其中,Name代表该字段名,Type代表该字段的类型:

type StructField struct {
	// Name is the field name.
	Name string

	// PkgPath is the package path that qualifies a lower case (unexported)
	// field name. It is empty for upper case (exported) field names.
	// See https://golang.org/ref/spec#Uniqueness_of_identifiers
	PkgPath string

	Type      Type      // field type
	Tag       StructTag // field tag string
	Offset    uintptr   // offset within struct, in bytes
	Index     []int     // index sequence for Type.FieldByIndex
	Anonymous bool      // is an embedded field
}

下面看一个生成结构体反射对象的例子。该函数可变参数中的类型依次构建为结构体的字段,并返回结构体变量:

func MakeStruct(vals ...any) reflect.Value {
	var fields []reflect.StructField
	for k, v := range vals {
		fields = append(fields, reflect.StructField{
			Name: fmt.Sprintf("Field%d", k),
			Type: reflect.TypeOf(v),
		})
	}
	return reflect.New(reflect.StructOf(fields))
}

func main() {
	// 构造结构体
	//struct {
	//	int
	//	string
	//	[]int
	//}{}
	obj := MakeStruct(0, "hello", []int{})
	obj.Elem().Field(0).SetInt(10)
	obj.Elem().Field(1).SetString("world")
	obj.Elem().Field(2).Set(reflect.ValueOf([]int{1, 2, 3}))
	fmt.Println(obj.Elem())
}

函数与反射

下例实现函数的动态调用,这和方法的调用是相同的,同样使用了reflect.Call。如果函数中的参数为指针,那么可以借助reflect.New生成指定类型的反射指针对象:

func Handler(args int, reply *int) {
	*reply = args
}

func main() {
	handler := reflect.ValueOf(Handler)
	args := []reflect.Value{reflect.ValueOf(10), reflect.New(reflect.TypeOf(0))}
	handler.Call(args)
}

反射与其他类型

对于其他的一些类型,可以通过XxxOf方法构造特定的reflect.Type类型,下例中介绍了一些复杂类型的反射实现过程:

func main() {
	ta := reflect.ArrayOf(5, reflect.TypeOf(0))                                 // [5]int
	tc := reflect.ChanOf(reflect.SendDir, ta)                                   // chan<- [5]int
	tp := reflect.PointerTo(ta)                                                 // *[5]int
	ts := reflect.SliceOf(tp)                                                   // []*[5]int
	tm := reflect.MapOf(ta, tc)                                                 // map[[5]int]chan<- [5]int
	tf := reflect.FuncOf([]reflect.Type{ta, tp}, []reflect.Type{ts, tm}, false) // func([5]int, *[5]int) ([]*[5]int, map[[5]int]chan<- [5]int)
	tt := reflect.StructOf([]reflect.StructField{
		{Name: "Age", Type: reflect.TypeOf("abc")},
	}) // struct{Age string}
}

根据reflect.Type生成对应的reflect.ValueReflect包中提供了对应类型的MakeXxx方法:

func MakeChan(typ Type, buffer int) Value
func MakeFunc(typ Type, fn func(args []Value) (results []Value)) Value
func MakeMap(typ Type) Value
func MakeMapWithSize(typ Type, n int) Value
func MakeSlice(typ Type, len, cap int) Value

除此之外,还可以使用reflect.New方法根据反射的类型分配相应大小的内存。

总结

反射为Go语言提供了复杂的、意想不到的处理能力及灵活性。这种灵活性以牺牲效率和可理解性为代价。通过反射,可以获取和修改变量自身的属性,构建一个新的结构,甚至进行动态的方法调用。虽然在实践中很少会涉及编写反射代码,但是反射确实在一些底层工具类代码和RPC远程过程调用中应用广泛(例如jsonxmlgrpcprotobuf)。