Go 译文之如何使用反射

poloxue · · 769 次点击 · · 开始浏览    
这是一个创建于 的文章,其中的信息可能已经有所发展或是发生改变。

[译文地址](https://juejin.im/post/5cf89f78f265da1b942139c3) 作者:Jon Bodner 地址:[Learning to Use Go Reflection](https://medium.com/capital-one-tech/learning-to-use-go-reflection-822a0aed74b7) # 什么是反射 多数情况下,Go 中的变量、类型和函数的使用都是非常简单的。 当你需要一个类型: ```go type Foo struct { A int B string } ``` 当你需要一个变量,定义如下: ```go var x Foo ``` 当你需要一个函数,定义如下: ```go func DoSomething(f Foo) { fmt.Println(f.A, f.B) } ``` 但有时候,你想使用的变量依赖于运行时信息,它们在编程时并不存在。比如数据来源于文件,或来源于网络,你想把它映射到一个变量,而它们可能是不同的类型。在这类场景下,你就需要用到反射。反射让你可以在运行时检查类型,创建、更新、检查变量以及组织结构。 Go 中的反射主要围绕着三个概念:类型(Types)、类别(Kinds)和值(Values)。反射的实现源码位于 Go 标准库 reflection 包中。 # 检查类型 首先,让我们来看看类型(Types)。你可以通过 reflect.TypeOf(var) 形式的函数调用获取变量的类型,它会返回一个类型为 reflect.Type 的变量,reflect.Type 中的操作方法涉及了定义该类型变量的各类信息。 我们要看的第一个方法是 Name(),它返回的是类型的名称。有些类型,比如 slice 或 指针,没有类型名称,那么将会返回空字符串。 下一个介绍方法是 Kind(),我的观点,这是第一个真正有用的方法。Kind,即类别,比如切片 slice、映射 map、指针 pointer、结构体 struct、接口 interface、字符串 string、数组 array、函数 function、整型 int、或其他的基本类型。type 和 kind 是区别不是那么容易理清楚,但是可以这么想: 当你定义一个名称为 Foo 的结构体,那么它的 kind 是 struct,而它的 type 是 Foo。 当使用反射时,我们必须要意识到:在使用 reflect 包时,会假设你清楚的知道自己在做什么,如果使用不当,将会产生 panic。举个例子,你在 int 类型上调用 struct 结构体类型上才用的方法,你的代码就会产生 panic。我们时刻要记住,什么类型有有什么方法可以使用,从而避免产生 panic。 如果一个变量是指针、映射、切片、管道、或者数组类型,那么这个变量的类型就可以调用方法 varType.Elem()。 如果一个变量是结构体,那么你就可以使用反射去得到它的字段个数,并且可以得到每个字段的信息,这些信息包含在 reflect.StructField 结构体中。reflect.StructField 包含字段的名称、排序、类型、标签。 前言万语也不如一行代码看的明白,下面的这个例子输出了不同变量所属类型的信息。 ```go type Foo struct { A int `tag1:"First Tag" tag2:"Second Tag"` B string } func main() { sl := []int{1, 2, 3} greeting := "hello" greetingPtr := &greeting f := Foo{A: 10, B: "Salutations"} fp := &f slType := reflect.TypeOf(sl) gType := reflect.TypeOf(greeting) grpType := reflect.TypeOf(greetingPtr) fType := reflect.TypeOf(f) fpType := reflect.TypeOf(fp) examiner(slType, 0) examiner(gType, 0) examiner(grpType, 0) examiner(fType, 0) examiner(fpType, 0) } func examiner(t reflect.Type, depth int) { fmt.Println(strings.Repeat("\t", depth), "Type is", t.Name(), "and kind is", t.Kind()) switch t.Kind() { case reflect.Array, reflect.Chan, reflect.Map, reflect.Ptr, reflect.Slice: fmt.Println(strings.Repeat("\t", depth+1), "Contained type:") examiner(t.Elem(), depth+1) case reflect.Struct: for i := 0; i < t.NumField(); i++ { f := t.Field(i) fmt.Println(strings.Repeat("\t", depth+1), "Field", i+1, "name is", f.Name, "type is", f.Type.Name(), "and kind is", f.Type.Kind()) if f.Tag != "" { fmt.Println(strings.Repeat("\t", depth+2), "Tag is", f.Tag) fmt.Println(strings.Repeat("\t", depth+2), "tag1 is", f.Tag.Get("tag1"), "tag2 is", f.Tag.Get("tag2")) } } } } ``` 输出如下: ``` Type is and kind is slice Contained type: Type is int and kind is int Type is string and kind is string Type is and kind is ptr Contained type: Type is string and kind is string Type is Foo and kind is struct Field 1 name is A type is int and kind is int Tag is tag1:"First Tag" tag2:"Second Tag" tag1 is First Tag tag2 is Second Tag Field 2 name is B type is string and kind is string Type is and kind is ptr Contained type: Type is Foo and kind is struct Field 1 name is A type is int and kind is int Tag is tag1:"First Tag" tag2:"Second Tag" tag1 is First Tag tag2 is Second Tag Field 2 name is B type is string and kind is string ``` [示例运行地址](https://play.golang.org/p/lZ97yAUHxX) # 创建实例 除了检查变量的类型外,你还可以利用来获取、设置和创建变量。首先,通过 refVal := reflect.ValueOf(var) 创建类型为 reflect.Value 的实例。如果你想通过反射来更新值,那么必须要获取到变量的指针 refPtrVal := reflect.ValueOf(&var),如果不这么做,那么你只能读取值,而不能设置值。 一旦得到变量的 reflect.Value,你就可以通过 Value 的 Type 属性获取变量的 reflect.Type 类型信息。 如果想更新值,记住要通过指针,而且在设置时,要先取消引用,通过 refPtrVal.Elem().Set(newRefVal) 更新其中的值,传递给 Set 的参数也必须要是 reflect.Value 类型。 如果想创建一个新的变量,可以通过 reflect.New(varType) 实现,传递的参数是 reflect.Type 类型,该方法将会返回一个指针,如前面介绍的那样,你可以通过使用 Elem().Set() 来设置它的值。 最终,通过 Interface() 方法,你就得到一个正常的变量。Go 中没有泛型,变量的类型将会丢失,Interface() 方法将会返回一个类型为 interface{} 的变量。如果你为了能更新值,创建的是一个指针,那么需要使用 Elem().Interface() 来获取变量。但无论是上面的哪种情况,你都需要把 interface{} 类型变量转化为实际的类型,如此才能使用。 下面是一些代码,实现了这些概念。 ```go type Foo struct { A int `tag1:"First Tag" tag2:"Second Tag"` B string } func main() { greeting := "hello" f := Foo{A: 10, B: "Salutations"} gVal := reflect.ValueOf(greeting) // not a pointer so all we can do is read it fmt.Println(gVal.Interface()) gpVal := reflect.ValueOf(&greeting) // it’s a pointer, so we can change it, and it changes the underlying variable gpVal.Elem().SetString("goodbye") fmt.Println(greeting) fType := reflect.TypeOf(f) fVal := reflect.New(fType) fVal.Elem().Field(0).SetInt(20) fVal.Elem().Field(1).SetString("Greetings") f2 := fVal.Elem().Interface().(Foo) fmt.Printf("%+v, %d, %s\n", f2, f2.A, f2.B) } ``` 输出如下: ```go hello goodbye {A:20 B:Greetings}, 20, Greetings ``` [示例运行地址](https://play.golang.org/p/PFcEYfZqZ8) # 无 make 的创建实例 对于像 slice、map、channel类型,它们需要用 make 创建实例,你也可以使用反射实现。slice 使用 reflect.MakeSlice,map 使用 reflect.MakeMap,channel 使用 reflect.MakeChan,你需要提供将创建变量的类型,即 reflect.Type,传递给这些函数。成功调用后,你将得到一个类型为 reflect.Value 的变量,你可以通过反射操作这个变量,操作完成后,就 可以将它转化为正常的变量。 ```go func main() { // declaring these vars, so I can make a reflect.Type intSlice := make([]int, 0) mapStringInt := make(map[string]int) // here are the reflect.Types sliceType := reflect.TypeOf(intSlice) mapType := reflect.TypeOf(mapStringInt) // and here are the new values that we are making intSliceReflect := reflect.MakeSlice(sliceType, 0, 0) mapReflect := reflect.MakeMap(mapType) // and here we are using them v := 10 rv := reflect.ValueOf(v) intSliceReflect = reflect.Append(intSliceReflect, rv) intSlice2 := intSliceReflect.Interface().([]int) fmt.Println(intSlice2) k := "hello" rk := reflect.ValueOf(k) mapReflect.SetMapIndex(rk, rv) mapStringInt2 := mapReflect.Interface().(map[string]int) fmt.Println(mapStringInt2) } ``` 输出如下: ``` [10] map[hello:10] ``` [运行示例](https://play.golang.org/p/z4tnyEf6bH) # 创建函数 你不仅经可以通过反射创建空间存储数据,还可以通过反射提供的函数 reflect.MakeFunc 来创建新的函数。这个函数期待接收参数有两个,一个是 reflect.Type 类型,并且 Kind 为 Function,另外一个是闭包函数,它的输入参数类型是 []reflect.Value,输出参数是 []reflect.Value。 下面是一个快速体验示例,可为任何函数在外层包裹一个记录执行时间的函数。 ```go func MakeTimedFunction(f interface{}) interface{} { rf := reflect.TypeOf(f) if rf.Kind() != reflect.Func { panic("expects a function") } vf := reflect.ValueOf(f) wrapperF := reflect.MakeFunc(rf, func(in []reflect.Value) []reflect.Value { start := time.Now() out := vf.Call(in) end := time.Now() fmt.Printf("calling %s took %v\n", runtime.FuncForPC(vf.Pointer()).Name(), end.Sub(start)) return out }) return wrapperF.Interface() } func timeMe() { fmt.Println("starting") time.Sleep(1 * time.Second) fmt.Println("ending") } func timeMeToo(a int) int { fmt.Println("starting") time.Sleep(time.Duration(a) * time.Second) result := a * 2 fmt.Println("ending") return result } func main() { timed := MakeTimedFunction(timeMe).(func()) timed() timedToo := MakeTimedFunction(timeMeToo).(func(int) int) fmt.Println(timedToo(2)) } ``` 输出如下: ``` starting ending calling main.timeMe took 1s starting ending calling main.timeMeToo took 2s 4 ``` [运行示例](https://play.golang.org/p/QZ8ttFZzGx) # 创建一个新的结构 Go 中,反射还可以在运行时创建一个全新的结构体,你可以通过传递一个 reflect.StructField 的 slice 给 reflect.StructOf 函数来实现。是不是听起来挺荒诞的,我们创建的一个新的类型,但是这个类型没有名字,因此也就无法将它转化为正常的变量。你可以通过它创建实例,用 Interface() 把它的值转给类型为 interface{} 的变量,但是如果要设置它的值,必须来反射来做。 ```go func MakeStruct(vals ...interface{}) interface{} { var sfs []reflect.StructField for k, v := range vals { t := reflect.TypeOf(v) sf := reflect.StructField{ Name: fmt.Sprintf("F%d", (k + 1)), Type: t, } sfs = append(sfs, sf) } st := reflect.StructOf(sfs) so := reflect.New(st) return so.Interface() } func main() { s := MakeStruct(0, "", []int{}) // this returned a pointer to a struct with 3 fields: // an int, a string, and a slice of ints // but you can’t actually use any of these fields // directly in the code; you have to reflect them sr := reflect.ValueOf(s) // getting and setting the int field fmt.Println(sr.Elem().Field(0).Interface()) sr.Elem().Field(0).SetInt(20) fmt.Println(sr.Elem().Field(0).Interface()) // getting and setting the string field fmt.Println(sr.Elem().Field(1).Interface()) sr.Elem().Field(1).SetString("reflect me") fmt.Println(sr.Elem().Field(1).Interface()) // getting and setting the []int field fmt.Println(sr.Elem().Field(2).Interface()) v := []int{1, 2, 3} rv := reflect.ValueOf(v) sr.Elem().Field(2).Set(rv) fmt.Println(sr.Elem().Field(2).Interface()) } ``` 输出如下: ``` 0 20 reflect me [] [1 2 3] ``` [运行示例](https://play.golang.org/p/lJiTP6vYYN) # 反射的限制 反射有一个大的限制。虽然运行时可以通过反射创建新的函数,但无法用反射创建新的方法,这也就意味着你不能在运行时用反射实现一个接口,用反射创建的结构体使用起来很支离破碎。而且,通过反射创建的结构体,无法实现 GO 的一个特性 —— 通过匿名字段实现委托模式。 看一个通过结构体实现委托模式的例子,通常情况下,结构体的字段都会定义名称。在这例子中,我们定义了两个类型,Foo 和 Bar: ```go type Foo struct { A int } func (f Foo) Double() int { return f.A * 2 } type Bar struct { Foo B int } type Doubler interface { Double() int } func DoDouble(d Doubler) { fmt.Println(d.Double()) } func main() { f := Foo{10} b := Bar{Foo: f, B: 20} DoDouble(f) // passed in an instance of Foo; it meets the interface, so no surprise here DoDouble(b) // passed in an instance of Bar; it works! } ``` [运行示例](https://play.golang.org/p/aeroNQ7bEI) 代码中显示,Bar 中的 Foo 字段并没有名称,这使它成了一个匿名或内嵌的字段。Bar 也是满足 Double 接口的,虽然只有 Foo 实现了 Double 方法,这种能力被称为委托。在编译时,Go 会自动为 Bar 生成 Foo 中的方法。这不是继承,如果你尝试给一个只接收 Foo 的函数传递 Bar,编译将不会通过。 如果你用反射去创建一个内嵌字段,并且尝试去访问它的方法,将会产生一些非常奇怪的行为。最好的方式就是,我们不要用它。关于这个问题,可以看下 github 的两个 issue,[issue/15924](https://github.com/golang/go/issues/15924) 和 [issues/16522](https://github.com/golang/go/issues/16522)。不幸的是,它们还没有任何的进展。 那么,这会有什么问题呢?如果支持动态的接口,我们可以实现什么功能?如前面介绍,我们能通过 Go 的反射创建函数,实现包裹函数,通过 interface 也可以实现。在 Java 中,这叫做动态代理。当把它和注解结合,将能得到一个非常强大的能力,实现从命令式编程方式到声明式编程的切换,一个例子 [JDBI](http://jdbi.org/),这个 Java 库让你可以在 DAO 层定义一个接口,它的 SQL 查询通过注解定义。所有数据操作的代码都是在运行时动态生成,就是如此的强大。 # 有什么意义 即使有这个限制,反射依然一个很强大的工具,每位 Go 开发者都应该掌握这项技能。但我们如何利用好它呢,[下一篇](https://medium.com/capital-one-tech/learning-to-use-go-reflection-part-2-c91657395066)博文再介绍,我将会通过一些库来探索反射的使用,并将利用它实现一些功能。 中文待续...

有疑问加站长微信联系(非本文作者))

入群交流(和以上内容无关):加入Go大咖交流群,或添加微信:liuxiaoyan-s 备注:入群;或加QQ群:692541889

769 次点击  
加入收藏 微博
暂无回复
添加一条新回复 (您需要 登录 后才能回复 没有账号 ?)
  • 请尽量让自己的回复能够对别人有帮助
  • 支持 Markdown 格式, **粗体**、~~删除线~~、`单行代码`
  • 支持 @ 本站用户;支持表情(输入 : 提示),见 Emoji cheat sheet
  • 图片支持拖拽、截图粘贴等方式上传