开篇

2011 年,Rob Pike 大神在介绍 Golang 反射的时候开篇提到了对于【反射】的定义:

Reflection in computing is the ability of a program to examine its own structure, particularly through types; it’s a form of metaprogramming. It’s also a great source of confusion.

反射是程序检查其自身结构的能力,特别是通过类型。这是元编程的一种形式。这也是一个很大的混乱来源。

作为一名 Golang 开发者,我常常对最后这句【a great source of confusion】感同身受。此前对于反射的理解总是带着一丝畏惧,毕竟是在运行时对结构进行操作,用法不当就会抛出 panic。并且资深的同事总是会告诉你,尽量少用反射,性能不理想。

后来接触开源社区的机会越来越多,发现其实很多业界知名的框架底层都依赖 reflect 包。妥善地处理好使用场景的话,它能够帮助你做到很多底层的事情。

今天这篇文章希望带大家看几个 reflect 实战的 showcase。

reflect 能力

反射最基本的两个方法,接收一个 interface{} 入参,返回对应的 Type / Value。

TypeOf(i interface{}) Type	// 对应着 interface 结构体中 *_type
ValueOf(i interface{}) Value	// 结合了data指针与 *_type 类型信息

通常情况下,这两个方法是我们使用反射能力的起点。下面我们分别针对 Type 和 Value 看看在完成转化后,reflect 提供了什么能力让我们使用。

建议大家有时间还是再仔细看看 src/reflect 包,这里我们把常用的拎出来。

Type

type Type interface {
	...
        
        Elem() Type				// 返回内部子元素类型
	Kind() Kind				// 所属基础类型
        
        // 接口实现
	Implements(u Type) bool

        // method 相关
	Method(int) Method	                // 返回类型方法集里的第 `i` (传入的参数)个方法
	MethodByName(string) (Method, bool) 	// 通过名称获取方法
	NumMethod() int		                // 方法个数
        
        // 类型转换相关
	AssignableTo(u Type) bool
	ConvertibleTo(u Type) bool
        
        // Field 相关
	Field(i int) StructField		// 获取结构体中的 字段
	FieldByName(name string) (StructField, bool)
	NumField() int
        ...
}

这里我们截取了一部分最常用的方法定义,如果你对 reflect 用的还不熟练,记住这些就 ok。需要注意的是,Type 定义的是一个大而全的 interface,但里面很多的方法是只有特定的类型才支持的,使用前一定要看好注释,否则很容易出 panic。

  • Elem: 获取指向的元素(element)的类型信息,支持的 Kind 为 Array, Chan, Map, Pointer, Slice;

  • Kind: 获取 Type 的类别,如 Int, Array, Struct, Func, Interface 等;

  • Implements: 传入一个接口定义,判断当前 Type 是否实现了此接口;

  • Method: 返回当前 Type 的第 i 个方法;

  • MethodByName: 根据名称返回方法;

  • NumMethod: 返回 Type 的方法数量,常用于 for 循环,可以与 Method 搭配使用;

  • AssignableTo: 判断两个类型的值能够直接赋值(类型相等,或者其中一个类型没有定义)

  • CovertibleTo: 判断两个类型 是否能够强制转换, 如 Int(32,64)之间,int 与 float,string 与 int32 都是能够转换的,代码中有转换的逻辑;

  • Field: 获取当前 Type 的第 i 个字段;

  • FieldByName: 根据名称获取字段;

  • NumField: 返回 Type 包含的字段数量。

Value

type Value struct {
	typ *rtype			// 类型
	ptr unsafe.Pointer		// 实际数据
	flag
}

和 Type 不同,Value 的类型是个 struct。除了 flag 标志位之外,就只剩 类型 + 数据的指针。

Value 实现的方法也有很多,这里节选一部分比较常用的:

  • Type: 返回具体类型;

  • Interface: 直接转为 interface{} 返回;

  • MethodByName: 根据名称获取方法;

  • FieldByName: 根据字段名找出字段对应的值,在 Type 有类似的方法,不过 Type 返回的是字段类型信息

  • Indirect: 如果这个值是指针,会返回指向的值,其中 flag 会加上 CanAddr 的标记,实际调用了 Elem() 实现;

// Indirect returns the value that v points to.
// If v is a nil pointer, Indirect returns a zero Value.
// If v is not a pointer, Indirect returns v.
func Indirect(v Value) Value {
	if v.Kind() != Pointer {
		return v
	}
	return v.Elem()
}
  • Elem: 返回接口或指针指向的对象, 返回的 value 能用 Addr 获取地址

  • CanAddr: 判断 value 能否通过 Addr 获取地址

  • CanSet: 判断 value 里的数据能否被改变,满足可寻址的条件(CanAddr),如果是字段是对外暴露的(字段名大写)

  • Convert: 改变 value 的具体类型,可以利用上面 Type 中提到的 ConvertibleTo 方法 来判断是否可以转换,如 int -> int64

  • Call: 调用的 Kind 必须是函数,用 in 作为参数,返回 []Value, 反射执行方法

  • Set: 改变 value 的值, 强调两件事, 一个调用者本身可以被设置, 即 value 满足 CanSet(),另外一个设置的值和调用者本身一致

Type 转换为 Value

当我们需要根据 Type 创建一个 Value 时,可以使用 reflect.New 函数:

// New返回一个Value, 该值表示指向指定类型的新零值的指针. 也就是说, 返回的值的类型为PtrTo(typ).
func New(typ Type) Value {
    if typ == nil {
        panic("reflect: New(nil)")
    }
    t := typ.(*rtype)
    ptr := unsafe_New(t)
    fl := flag(Ptr)
    return Value{t.ptrTo(), ptr, fl}
}

New 和 NewAt 一样, 都是构建了一个 Ptr 类型 Value. 但是区别在于, NewAt 的指针是外部的, 而 New 的指针 是新创建的.

实战案例

解析结构体 tag

Golang 的 tag 是对于结构体定义非常好的能力扩充,我们可以使用 reflect.Type 的能力,获取到某个字段定义里面 tag 的相关信息。

这里依赖的方法如下:

  • TypeOf: 将目标结构体传入,拿到 Type;
  • NumField: 可选,如果我们需要遍历结构体的话,可以用这个获取到 field 数量,通常在 for 循环里面用;
  • Field: 也可以用其他 FieldXXX 方法替代,这里本质是根据字段在结构体中的顺序,来获取单独的字段,入参 i 需要在 [0, NumField()) 这个范围内。

流程很简单:

  1. 结构体传入,通过 reflect.TypeOf 转为 Type;
  2. 拿到指定的 Field,可以通过 NumField 然后 Field(i) 遍历,也可以通过 index 或者 name,随需而定。此处拿到的 Field 本质是一个 reflect.StructField 结构,代表了结构体中的一个字段:
// A StructField describes a single field in a struct.
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
}

type StructTag string

这里也可以看到,我们可以从 StructField 里的 Tag 字段拿到标签相关的数据;

  1. 有了 Tag,调用 StructTag 这个重定义字符串的类型实现的 Lookup(key string) (value string, ok bool) 方法即可获取指定 tag key 的值。

仔细看 StructTag 你会发现还有个Get(key string) string 方法,其实本质就是调用了 Lookup,但是丢弃了标记 key 是否存在的 bool 罢了。如果你需要判断是否存在,建议用 Lookup。

完整的 demo 如下,其实流程一点都不复杂,大家可以熟悉一下:

package main

import (
	"fmt"
	"reflect"
)

type MyStruct struct {
	Location string `customTag:"custom value"`
}

func main() {
	t := reflect.TypeOf(MyStruct{})
	for i := 0; i < t.NumField(); i++ {
		field := t.Field(i)
		tag := field.Tag
		fmt.Printf("tag=%+v\n", tag)

		customTagVal := tag.Get("customTag")
		fmt.Printf("customTagVal=%s\n", customTagVal)

		lookVal, exist := tag.Lookup("customTag")
		fmt.Printf("lookVal=%s, exist=%v\n", lookVal, exist)
	}
}

运行程序后打印的结果:

tag=customTag:"custom value"
customTagVal=custom value
lookVal=custom value, exist=true

对象之间复制

我们希望实现对象之间的复制,能否用 reflect 实现呢?

一句话,实现 func Copy(dest, src interface{}) error 函数。

这类最经典的实现还是 jinzhu/copier, 不过 copier 本身提供的能力比较多,各种边界 case 基本也都处理了。此处我们并不执着于写出来一个方方面面没 bug,各种结构都支持的 copier,假设是个简单的 struct to struct 场景,我们应该怎样实现呢?

先来理一下思路:

  1. 首先,Golang 是值传递,所以 dest 一定需要是个指针,否则没有意义。我们需要拿到 dest 指向的值的 reflect.Value;
  2. 同样的,对于 src 也需要拿到对应的 reflect.Value;
  3. 遍历 src 的所有字段(这个可以用我们上一节提到的 NumField 遍历),拿到一个个 field 后,到 dest 结构里找【同名字段】;
  4. 现在 src 和 dest 的同名字段找到了,我们需要拿到 src field 的值,赋给 dest field。这里可以用 Type 提供的 ConvertibleTo 方法,判断能不能转换;
  5. 若可以转换,直接使用 Value 的 Convert 方法将 src field 转换过去,就有了 dest field 的值;
  6. 调用 Value 的 Set 方法,将 dest field 更新为上一步获得的值。

其实很多框架实现,最后都会具体落到某个结构体里的 field 上,我们参照上面的思路写一下代码:

func Copy(dest, src interface{}) error {
	destValue := reflect.Indirect(reflect.ValueOf(dest))
	if !destValue.CanAddr() {
		return errors.New("dest type is not ptr")
	}

	srcValue := reflect.ValueOf(src)
	srcType := reflect.TypeOf(src)
	if srcType.Kind() == reflect.Ptr {
		srcType = srcType.Elem()
	}
	for i := 0; i < srcType.NumField(); i++ {
		fieldSrc := srcType.Field(i)
		fieldDest, exist := destValue.Type().FieldByName(fieldSrc.Name)
		if !exist {
			continue
		}
		if ok := fieldSrc.Type.ConvertibleTo(fieldDest.Type); ok {
			convertValue := srcValue.FieldByName(fieldSrc.Name).Convert(fieldDest.Type)
			destValue.FieldByName(fieldSrc.Name).Set(convertValue)
		}
	}
	return nil
}

配合一个测试 case 我们来看一下效果:

package main

import (
	"errors"
	"fmt"
	"reflect"
)

func main() {
	type A struct {
		Name     string
		Age      int
		Location string
	}
	type B struct {
		Name     string
		Location string
	}
	a := A{
		Name:     "a",
		Age:      1,
		Location: "Kenmawr",
	}

	b := &B{}
	fmt.Printf("before Copy, b=%+v\n", b)
	Copy(b, a)
	fmt.Printf("after Copy, b=%+v\n", b)
}

最后输出的结果为

before Copy, b=&{Name: Location:}
after Copy, b=&{Name:a Location:Kenmawr}
复制代码

这样一个简单的 copier 就实现了。其实我们还有非常多 case 没有考虑到,感兴趣的同学可以看看 jinzhu/copier 源码,整体代码量并不多,虽然性能上有改进空间,但是还是有很大学习价值的。

依赖注入

前几天我们学习了 goioc/dic 这个注入框架,可以回顾下: Golang 依赖注入库 goioc/di 用法和原理解析

今天我们把反射的部分单拎出来看看。要解决的问题很简单:我是一个结构体,存在成员变量需要注入,我知道它的实现是什么,怎么替换过来?

func injectField(fieldToInject reflect.Value, val interface{}) {
	fieldToInject = reflect.NewAt(fieldToInject.Type(), unsafe.Pointer(fieldToInject.UnsafeAddr())).Elem()
	fieldToInject.Set(reflect.ValueOf(val))
}

其实很简单,unsafe.Pointer(fieldToInject.UnsafeAddr()) 获取当前 field 的地址,通过 NewAt 获取对应 field 的指针,然后一次 Set,传入你要注入的实现即可。

这里重点介绍一下 reflect.NewAt 函数。我们知道,struct 中的小写字段是无法导出的,那么依赖注入的时候就会出现问题。我们希望这种场景也能支持。这个时候就可以用 NewAt 的能力了。

// NewAt返回一个Value, 该指针表示一个指向指定类型, 使用p作为该指针.
func NewAt(typ Type, p unsafe.Pointer) Value {
    fl := flag(Ptr)
    t := typ.(*rtype)
    return Value{t.ptrTo(), p, fl}
}

NewAt 的实质上是构建了一个指针类型 reflect.Value, 该 Value 具有 CanInterface() 的特性。 经过 Elem() 之后, 又会增加 CanSet()CanAddr() 两个特性。指针的指向正是传入的地址 p. 要想获取 "值 (结构体) 类型" 的 reflect.Value, 只需要对返回值再执行 Elem() 操作即可.

而 UnsafeAddr 提供了返回指针的能力,二者搭配就能做到我们需要的效果了。

// UnsafeAddr 返回指向 v 数据的指针.
// 适用于高级操作, 这些操作需要导入了 "unsafe" 包.
// 如果v不可寻址, 它会 panic
func (v Value) UnsafeAddr() uintptr {
    if v.typ == nil {
        panic(&ValueError{"reflect.Value.UnsafeAddr", Invalid})
    }
    if v.flag&flagAddr == 0 {
        panic("reflect.Value.UnsafeAddr of unaddressable value")
    }
    return uintptr(v.ptr)
}

由此我们可以抽象出以下两个能力

  1. 获取非导出字段
func GetPtrUnExportFiled(s interface{}, filed string) reflec.Value {
    v := reflect.ValueOf(s).Elem().FiledByName(filed)
    // 必须要调用 Elem()
    return reflect.NewAt(v.Type(), unsafe.Pointer(v.UnsafeAddr())).Elem()
}
  1. 设置非导出字段的值
func SetPtrUnExportFiled(s interface{}, filed string, val interface{}) error {
     v := GetPtrUnExportFiled(s, filed)
     rv := reflect.ValueOf(val)
     if v.Kind() != v.Kind() {
        return fmt.Errorf("invalid kind, expected kind: %v, got kind:%v", v.Kind(), rv.Kind())
     }

     v.Set(rv)
     return nil
}

我们前面提到过, New 返回的是一个新的地址,由于这个原因, 无法修改本身的字段的值, NewAt 则是复用指针,这样就解决了问题。

参考资料

The Laws of Reflection

reflect 修改对象非导出字段的值