开篇
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()) 这个范围内。
流程很简单:
- 结构体传入,通过 reflect.TypeOf 转为 Type;
- 拿到指定的 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 字段拿到标签相关的数据;
- 有了 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 场景,我们应该怎样实现呢?
先来理一下思路:
- 首先,Golang 是值传递,所以 dest 一定需要是个指针,否则没有意义。我们需要拿到 dest 指向的值的 reflect.Value;
- 同样的,对于 src 也需要拿到对应的 reflect.Value;
- 遍历 src 的所有字段(这个可以用我们上一节提到的 NumField 遍历),拿到一个个 field 后,到 dest 结构里找【同名字段】;
- 现在 src 和 dest 的同名字段找到了,我们需要拿到 src field 的值,赋给 dest field。这里可以用 Type 提供的 ConvertibleTo 方法,判断能不能转换;
- 若可以转换,直接使用 Value 的 Convert 方法将 src field 转换过去,就有了 dest field 的值;
- 调用 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)
}
由此我们可以抽象出以下两个能力
- 获取非导出字段
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()
}
- 设置非导出字段的值
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 则是复用指针,这样就解决了问题。