摘要
本文通过使用Golang的反射功能,设置结构体的成员变量值,通过阅读这个过程中的源代码,理解反射的实现原理。
一、rtype结构体
在golang的reflect实现中大量使用了空接口,空接口在源码实现中用emptyInterface结构体表示,代码如下所示。
type emptyInterface struct {
typ *rtype // 指向实际数据的类型信息
word unsafe.Pointer // 指向数据在内存中的地址
}
golang中每种数据类型(比如int32,long,slice,指针以及用户自定义数据结构)都会包含一个rtype结构体,rtype描述了每种类型的通用信息,每种具体的类型(比如指针类型、结构类型)都是rtype + 具体类型特有信息。以结构体、指针类型为例,它们在源码中的数据结构如下代码。
// 结构体类型
type structType struct {
rtype // 结构体自身的类型信息
pkgPath name
fields []structField // 结构体每个成员变量的信息
}
// 指针类型
type ptrType struct {
rtype // 指针类型
elem *rtype // 指针指向的数据的类型
}
在实现时,总是将rtype作为类型的表示,具体的类型在需要的时候通过强制类型转换实现,例如图1是reflect包中的一段代码,将rtype强制转换为structType。从上述代码可以看出指针类型和结构体类型在内存中的布局如图2。
func (t *rtype) FieldByName(name string) (StructField, bool) {
tt := (*structType)(unsafe.Pointer(t))
}
图1
图2
二、示例
本示例,通过反射设置结构体的成员变量值,分析代码的执行流程和涉及的数据结构。示例中定义一个User结构体,取地址赋值给空接口,通过反射方法改变User结构体的成员变量值。
package main
import (
"fmt"
"reflect"
)
type User struct {
Name string
Phone string
Age int32
}
func main() {
user := User{}
var eface interface{} = &user // 将指针以及指针指向的数据保存在空接口中
value := reflect.ValueOf(eface) // 生成反射对象Value
elem := value.Elem() // 获得指针指向的对象Value
field := elem.FieldByName("Phone") // 找到Phone成员变量
field.SetString("0755-8000") // 设置Name变量的值为rtx
fmt.Println(user.Name) // 输出“0755-8000”
}
定义的变量user在内存中的布局如图3,在golang的string的实现中,每个string占用16个字节,x是user结构的起始地址,Name的偏移量为0,Phone的偏移量为16,Age的偏移量为32。
图3
var eface interface{} = &user
上面这条语句,经过编译器编译之后,会生成图4的数据结构,其中空接口eface的typ字段保存的类型代表了&user类型(指针类型),word指针指向user在内存中的地址。
图4
reflect中的Value结构体代表了go变量的反射对象,Value的数据结构如图5。
type Value struct {
typ *rtype // 类型信息
ptr unsafe.Pointer // 可以指向变量的地址
flag // 变量的一下元信息标志
}
图5
reflect的ValueOf方法将空接口转换为Value结构体,ValueOf主要调用unpackEface函数,unpackEface函数首先将空接口i强制转换为emptyInterface地址,然后取得eface里面的typ字段和word字段,而这两个字段正是go变量的类型字段和内存地址。
func ValueOf(i interface{}) Value {
......
return unpackEface(i)
}
// 转换空接口为Value
func unpackEface(i interface{}) Value {
e := (*emptyInterface)(unsafe.Pointer(&i)) // 地址强制转换
t := e.typ // 取得类型结构指针
......
return Value{t, e.word, f} // 构造Value结构体
}
通过上面的分析,在执行了如下代码后,会有如图6的数据结构。
value := reflect.ValueOf(eface)
value结构体中typ保存的是&user类型,即ptrType,ptrType主要保存两个信息,第一是指针本身的类型信息。第二个是ptrType中elem成员,表示指针指向的数据类型(这里是User类型),user是User类型的结构体,golang用structType数据结构表示结构体类型。
图6
此时,Value中的typ是指针类型,ptr指向user结构体。ptrType结构体中的*elem是指针指向的类型,要想通过反射设置结构体中成员变量的值,必须找到User类型。Value的Elem方法可以达到此目的。Elem的代码如图7所示,Elem生成一个新的value结构体,type指向structType,ptr仍然指向user结构体。
func (v Value) Elem() Value {
k := v.kind() // value是一个指针类型
switch k {
case Ptr:
ptr := v.ptr // 直接复制ptr指针
......
tt := (*ptrType)(unsafe.Pointer(v.typ)) // rtype类型强制转换为ptrType
typ := tt.elem // 获取指针指向的数据类型,这里是User结构体类型
......
return Value{typ, ptr, fl} // 构造新的Value结构
}
.....
}
图7
通过以上代码分析,执行完如下代码后,会生成新的Value实例elem,数据结构之间的关系如图8所示。
elem := value.Elem()
图8
structType的数据结构如图9,我们定义的User结构体有三个成员Name,Phone,Age,编译器会生成如图9的数据结构,每个成员变量用一个structField结构体表示,每一个成员变量都有自己的类型,用typ成员表示。在User结构体中,Name和Phone是string类型,Age是int32类型。structField的offsetEmbed,表示该字段在结构体中的内存偏移量,该偏移量可以帮助定位结构体成员。
图9
把图8和图9的数据结构画在一起,如图10。structField的offsetField变量的最低位用于表示字段是否是嵌入字段,图中为了方便,忽略了最低位直接用0,16,32表示偏移量。
图10
要改变结构体的成员变量值,需要找到该结构成员在内存中的位置,可以通过FieldByName方法找到。该方法首先执行v.typ.FieldByName(name),将rtype强制转换为structType,接着调用structType的FieldByName方法,该方法遍历图10中的structType的field切片,找到跟输入参数name匹配的成员变量名,然后返回该structFiled结构体指针,structFiled中的index表示该成员变量在结构中的位置,可以认为该index就是structType中filed切片中的下标,通过该index可以找到structField结构。
func (v Value) FieldByName(name string) Value {
....
if f, ok := v.typ.FieldByName(name); ok { // 通过成员变量名找到代表成员的structField
return v.FieldByIndex(f.Index) // 通过index找到structField,返回Value
}
return Value{}
}
func (t *rtype) FieldByName(name string) (StructField, bool) {
tt := (*structType)(unsafe.Pointer(t)) // 强制类型转换
return tt.FieldByName(name) // 通过结构体成员变量名遍历field切片
}
// and a boolean to indicate if the field was found.
func (t *structType) FieldByName(name string) (f StructField, present bool) {
....
if name != "" {
for i := range t.fields { // 遍历结构体的每一个成员变量
tf := &t.fields[i]
if tf.name.name() == name { // 比较成员变量名
return t.Field(i), true // 返回匹配的结果
}
}
}
func (v Value) FieldByIndex(index []int) Value {
if len(index) == 1 {
return v.Field(index[0])
}
....
}
// 获得结构体的第i个成员
func (v Value) Field(i int) Value {
tt := (*structType)(unsafe.Pointer(v.typ))
field := &tt.fields[i] // 第i个成员的信息
typ := field.typ // 第i个成员的类型
// v.ptr指向了user的内存地址,field.Offset()方法是结构体成员变量在内存中偏移量
// v.ptr + offset就是成员变量的地址
ptr := add(v.ptr, field.offset(), "")
return Value{typ, ptr, fl} // 返回的Value代表了结构体的某一成员变量
}
通过上述分析,在执行下面的语句后,会有如图11所示的数据结构, 新生成的Value是field。
field := elem.FieldByName("Phone")
图11
field的ptr指针指向user内存起始地址偏移量16位置,然后执行下面的语句
field.SetString("0755-8000")
field的SetString方法直接操作ptr指向的内存地址,offset=16处的地址,正是Phone成员变量所在的地址,这就完成了通过反射设置User结构体的Phone成员变量。
func (v Value) SetString(x string) {
....
*(*string)(v.ptr) = x
}