golang reflect源码浅析

摘要

          本文通过使用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
}

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值