反射在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.TypeOf
与reflect.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.Type
。reflect.Value
与reflect.Type
都具有Kind
方法,可以获取标识类型的Kind
,其底层是unit8
。Go
语言中的内置类型都可以用唯一的整数进行标识:
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
还提供了一些转换到具体类型的方法,这些特殊的方法可以加快转换的速度。另外,这些方法经过了特殊的处理,因此不管反射内部类型是int8
、int16
,还是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)
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.Value
的Set
方法:
func (v Value) Set(x Value)
该方法的参数仍然是reflect.Value
,但是要求反射中的类型必须是指针。
这与禁止用值类型去调用指针接收者的方法的原理是一样的。为了避免这种错误,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.Type
与reflect.Value
都有Field
方法,relect.Type
的Field
方法主要用于获取结构体的元信息,其返回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.Value
的Field
方法主要返回结构体字段的值类型,后续可以使用它修改结构体字段的值。
要修改结构体字段,可以使用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.Value
的MethodByName
方法,参数为方法名并返回代表该方法的reflect.Value
对象。如果该方法不存在,则会返回空。
如下所示,通过Type
方法将reflect.Value
转换为reflect.Type
,reflect.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标准库中的ArrayOf
、SliceOf
等函数生成一些在编译时完全不存在的类型或对象。对于结构体,需要使用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.Value
,Reflect
包中提供了对应类型的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远程过程调用中应用广泛(例如json
、xml
、grpc
、protobuf
)。