19

Go 译文之如何使用反射

 5 years ago
source link: https://www.tuicool.com/articles/mIFfeqj
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

译文地址

作者: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 开发者都应该掌握这项技能。但我们如何利用好它呢, 下一篇 博文再介绍,我将会通过一些库来探索反射的使用,并将利用它实现一些功能。

中文待续...


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK