Go 译文之如何使用反射

3,538 阅读9分钟

作者:Jon Bodner | 地址:Learning to Use Go Reflection

什么是反射

多数情况下,Go 中的变量、类型和函数的使用都是非常简单的。

当你需要一个类型,定义如下:

type Foo struct {
  A int
  B string
}

当你需要一个变量,定义如下:

var x Foo

当你需要一个函数,定义如下:

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 包含字段的名称、排序、类型、标签。

前言万语也不如一行代码看的明白,下面的这个例子输出了不同变量所属类型的信息。


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

运行示例

创建实例

除了检查变量的类型外,你还可以利用来获取、设置和创建变量。首先,通过 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{} 类型变量转化为实际的类型,如此才能使用。

下面是一些代码,实现了这些概念。

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)
}

输出如下:

hello
goodbye
{A:20 B:Greetings}, 20, Greetings

运行示例

无 make 的创建实例

对于像 slice、map、channel类型,它们需要用 make 创建实例,你也可以使用反射实现。slice 使用 reflect.MakeSlice,map 使用 reflect.MakeMap,channel 使用 reflect.MakeChan,你需要提供将创建变量的类型,即 reflect.Type,传递给这些函数。成功调用后,你将得到一个类型为 reflect.Value 的变量,你可以通过反射操作这个变量,操作完成后,就 可以将它转化为正常的变量。

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]

运行示例

创建函数

你不仅经可以通过反射创建空间存储数据,还可以通过反射提供的函数 reflect.MakeFunc 来创建新的函数。这个函数期待接收参数有两个,一个是 reflect.Type 类型,并且 Kind 为 Function,另外一个是闭包函数,它的输入参数类型是 []reflect.Value,输出参数是 []reflect.Value。

下面是一个快速体验示例,可为任何函数在外层包裹一个记录执行时间的函数。


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

运行示例

创建一个新的结构

Go 中,反射还可以在运行时创建一个全新的结构体,你可以通过传递一个 reflect.StructField 的 slice 给 reflect.StructOf 函数来实现。是不是听起来挺荒诞的,我们创建的一个新的类型,但是这个类型没有名字,因此也就无法将它转化为正常的变量。你可以通过它创建实例,用 Interface() 把它的值转给类型为 interface{} 的变量,但是如果要设置它的值,必须来反射来做。

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]

运行示例

反射的限制

反射有一个大的限制。虽然运行时可以通过反射创建新的函数,但无法用反射创建新的方法,这也就意味着你不能在运行时用反射实现一个接口,用反射创建的结构体使用起来很支离破碎。而且,通过反射创建的结构体,无法实现 GO 的一个特性 —— 通过匿名字段实现委托模式。

看一个通过结构体实现委托模式的例子,通常情况下,结构体的字段都会定义名称。在这例子中,我们定义了两个类型,Foo 和 Bar:

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!
}

运行示例

代码中显示,Bar 中的 Foo 字段并没有名称,这使它成了一个匿名或内嵌的字段。Bar 也是满足 Double 接口的,虽然只有 Foo 实现了 Double 方法,这种能力被称为委托。在编译时,Go 会自动为 Bar 生成 Foo 中的方法。这不是继承,如果你尝试给一个只接收 Foo 的函数传递 Bar,编译将不会通过。

如果你用反射去创建一个内嵌字段,并且尝试去访问它的方法,将会产生一些非常奇怪的行为。最好的方式就是,我们不要用它。关于这个问题,可以看下 github 的两个 issue,issue/15924issues/16522。不幸的是,它们还没有任何的进展。

那么,这会有什么问题呢?如果支持动态的接口,我们可以实现什么功能?如前面介绍,我们能通过 Go 的反射创建函数,实现包裹函数,通过 interface 也可以实现。在 Java 中,这叫做动态代理。当把它和注解结合,将能得到一个非常强大的能力,实现从命令式编程方式到声明式编程的切换,一个例子 JDBI,这个 Java 库让你可以在 DAO 层定义一个接口,它的 SQL 查询通过注解定义。所有数据操作的代码都是在运行时动态生成,就是如此的强大。

有什么意义

即使有这个限制,反射依然一个很强大的工具,每位 Go 开发者都应该掌握这项技能。但我们如何利用好它呢,下一篇英文原版博文再介绍,我将会通过一些库来探索反射的使用,并将利用它实现一些功能。


波罗学的微信公众号