【Go基础】反射

415 阅读7分钟

反射

推荐学习:【Golang】图解反射

前言

用到反射的场景不多,一般是变量类型不确定,内部结构不明朗的情况,反正我现在一次都没有用过,但是不妨碍我们来看看底层到底是怎么回事。

reflect 有两个核心类型:reflect.Typereflect.Value。他们撑起了反射功能的基本框架。

Type

reflect.Type 是一个接口类型,定义了一系列方法获取类型各方面的信息。

type Type interface {
    Align() int //对齐边界
    FieldAlign() int //作为结构体字段的对齐边界
    Method(int) Method //获取方法数组中第i个Method
    MethodByName(string) (Method, bool) //按照名称查找方法
    NumMethod() int //方法列表中可导出方法的数目
    Name() string //类型名称
    PkgPath() string //包路径
    Size() uintptr //该类型变量占用字节数
    String() string //获取类型的字符串表示
    Kind() Kind //类型对应的reflect.Kind
    Implements(u Type) bool //该类型是否实现了接口u
    AssignableTo(u Type) bool //是否可以赋值给类型u
    ConvertibleTo(u Type) bool //是否可转换为类型u
    Comparable() bool //是否可比较

    //只能应用于某些Kind的方法
    //Int*, Uint*, Float*, Complex*: 
    Bits() int

    //Array,Ptr,Slice,Map: 
    Elem() Type
    //Array
    Len() int
        
    //Chan:ChanDir, Elem 
    ChanDir() ChanDir

    //Func: 
    In(i int) Type
    NumIn() int
    Out(i int) Type
    NumOut() int
    IsVariadic() bool

    //Map: 
    Key() Type

    //Struct: 
    Field(i int) StructField
    FieldByIndex(index []int) StructField
    FieldByName(name string) (StructField, bool)
    FieldByNameFunc(match func(string) bool) (StructField, bool)  
    NumField() int

    common() *rtype
    uncommon() *uncommonType
}

我们通常会用 reflect.TypeOf 这个函数去拿到 reflect.Type 类型的返回值、

// TypeOf 返回表示 i 的动态类型的反射类型。如果 i 是一个 nil 接口值,则 TypeOf 返回 nil。
func TypeOf(i interface{}) Type {
	eface := *(*emptyInterface)(unsafe.Pointer(&i))
	return toType(eface.typ)
}

举个例子🌰:

	a:="eggo"
	t:=reflect.TypeOf(a)
	fmt.Println(t.Name())

我们要反射 int 类型的变量 a

我们依旧从函数调用栈开始分析,main 函数栈帧中有两个局部变量 ta,然后是返回值空间,最后是参数。我们知道 Go 语言中参数都是值拷贝。难道这里参数是拷贝变量 a 的值吗?不行,TypeOf 函数的参数类型是空接口。

type eface struct {
    _type *_type //指向接口的动态类型元数据 
    data  unsafe.Pointer //指向接口的动态值。 
}

_type 字段接受的是 a 的动态类型,data 接受的是 a 的地址。

拷贝不了值,那拷贝变量 a 的地址?也不行,因为这样并不符合Go 语言中传参值拷贝的语义,无论对参数做怎么样的修改,都不能作用到局部变量。

**既然拷贝不了值,也不能拷贝值,那应该怎么办?**实际上在编译阶段会增加一个临时变量作为 a 的拷贝值(copy of a),再把这个临时变量的地址传递给函数使用,这样无论函数对参数做怎么样的修改,都不会作用到局部变量 a。既满足了空接口类型的参数也符合传参值拷贝的语义。

图片

当参数是空接口情况,都是通过传递拷贝后变量的地址来实现传值的语义。

接下来,reflect.TypeOf 函数会把 runtime.eface 类型的参数 i 转化成 reflect.emptInterface 类型并赋给变量 eface,转换以后方便 reflect 包操作内部元素。

// emptyInterface 是 interface{} 值的标头。
type emptyInterface struct {
	typ  *rtype //string
	word unsafe.Pointer //->i.data
}

因为 *rtype 实现了 reflect.Type 接口,TypeOf 函数要做的就是把 efacetyp 字段取出来,包装成 reflect.Type 类型的返回值就可以了,那么这就是 toType 函数干的活

func toType(t *rtype) Type {
	if t == nil {
		return nil
	}
	return t
}
//上面的代码可以等价于
var ret reflect.Type
ret = eface.typ

reflect.Type 既然是一个非空接口,那么他的结构如下:

type iface struct {
    tab   *itab
    data  unsafe.Pointer //接口的动态值 ->eface.typ
}
//itab
type itab struct {
    inter  *interfacetype //接口的类型元数据  ->reflect.Type
    _type  *_type //指向接口的动态类型元数据 ->*rtype
    hash   uint32 //itab._type中拷贝来的,类型哈希值
    _      [4]byte
    fun    [1]uintptr //动态类型实现的接口要求方法地址 
}

那么最后返回值的结构就与 iface 结构差不多,tab 指向 <reflect.Type,*rtype> 对应的 itab 指针,data 则是变量 a 的地址。

接下来通过非空接口类型 t 去调用各种方法都会去 string 类型查找相关数据。

Value

reflect.Value 是一个结构体类型。

type Value struct {
    typ *rtype //存储反射变量的类型元数据指针
    ptr unsafe.Pointer //存储数据地址
    flag //是一个位标识符,存储反射变量值的一些描述信息,例如类型掩码,是否为指针,是否为方法,是否只读等等
}

我们来看看反射是如何更新变量值的!

举个例子🌰:

	a := "eggo"
	v := reflect.ValueOf(a)
	v.SetString("new eggo")
	println(a)
//panic: reflect: reflect.Value.SetString using unaddressable value

我们来看看 reflect.ValueOf 函数是怎么样的!

func ValueOf(i interface{}) Value {
	if i == nil {
		return Value{}
	}
	escapes(i)

	return unpackEface(i)
}

func escapes(x interface{}) {
	if dummy.b {
		dummy.x = x
	}
}

他的参数是空接口类型,所以接受的参数数据是变量 a 的拷贝值地址。

type eface struct {
    _type *_type //指向接口的动态类型元数据  ->stringtype
    data  unsafe.Pointer //指向接口的动态值。  ->&(copy of a)
}

根据代码注释,escapes 函数会把参数 i 指向得变量(变量 a 得拷贝值 copy of a)逃逸到堆上,那么栈上只会留下他(copy of a)的地址。

图片

func unpackEface(i interface{}) Value {
	e := (*emptyInterface)(unsafe.Pointer(&i))
	// NOTE: don't read e.word until we know whether it is really a pointer or not.
	t := e.typ
	if t == nil {
		return Value{}
	}
	f := flag(t.Kind())
	if ifaceIndir(t) {
		f |= flagIndir
	}
	return Value{t, e.word, f}
}

简单看这个函数逻辑。ValueOf 函数的返回值是 reflect.Value 类型,返回值结构如下:

type Value struct {
    typ *rtype //存储反射变量的类型元数据指针  ->就是参数的 _type
    ptr unsafe.Pointer //存储数据地址 ->&(copy of a) 根据 unpackEface 来看它来自于 emptyInterface.word 再往前看 其实来自于 参数的 data 字段
    flag //是一个位标识符,存储反射变量值的一些描述信息,例如类型掩码,是否为指针,是否为方法,是否只读等等
}

这个返回值会被赋值给局部变量 v,调用 SetString

func (v Value) SetString(x string) {
	v.mustBeAssignable()
	v.mustBe(String)
	*(*string)(v.ptr) = x
}

SetString 函数会用到 Value 类型的 ptr 字段。但是经过返回值赋值局部变量 v 他的字段 ptr 好像指向的是 &(copy of a),这是临时变量的地址,不能修改的。所以就会报错。

因为修改这样一个连用户都不知道的临时变量是没有任何意义,同时我们的本意是要修改局部变量 a,所以就会发生 panic

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

好像这样子去修改不符合要求啊!

我们回忆一下,为什么会发生 panic?我们从后面一步一步理一下,失败原因。

  • v 的字段 ptr 指向是 &(copy of a)
  • 为什么会有这个 &(copy of a)?因为传参传的是局部变量 a,为了满足语义。

我们现在明白了,要想去修改变量 a,需要反射 a 的指针,只有这样 ptr 才会指向 &a

举个例子🌰:

func main() {
    a := "eggo"
    v := reflect.ValueOf(&a)
    v.Elem().SetString("new eggo")
    println(a)
}

这里传参依旧是值拷贝,这次拷贝 a 的地址,escapes 会将参数指向的变量 a 逃逸到堆上,那么栈上只存他的地址 &a

图片

剩余的部分与上面那个例子类似,就不过多的描述了,我们来看看不同的地方。

通过调用 v.Elem() 方法然后,再调用的的 SetString 方法就可以修改原来的变量 a了。

为什么一定要先调用 Elem 拿到 ptr 再去修改值呢?

func (v Value) Elem() Value {
	k := v.kind()
	switch k {
	case Interface:
		var eface interface{}
		if v.typ.NumMethod() == 0 {
			eface = *(*interface{})(v.ptr)
		} else {
			eface = (interface{})(*(*interface {
				M()
			})(v.ptr))
		}
		x := unpackEface(eface)
		if x.flag != 0 {
			x.flag |= v.flag.ro()
		}
		return x
	case Ptr:
		ptr := v.ptr
		if v.flag&flagIndir != 0 {
			ptr = *(*unsafe.Pointer)(ptr)
		}
		// The returned value's address is v's value.
		if ptr == nil {
			return Value{}
		}
		tt := (*ptrType)(unsafe.Pointer(v.typ))
		typ := tt.elem
		fl := v.flag&flagRO | flagIndir | flagAddr
		fl |= flag(typ.Kind())
		return Value{typ, ptr, fl}
	}
	panic(&ValueError{"reflect.Value.Elem", v.kind()})
}
func (v Value) SetString(x string) {
	v.mustBeAssignable()
	v.mustBe(String)
	*(*string)(v.ptr) = x
}