GO反射(reflect)学习[翻译]

  • A+
所属分类:golang

文章来自Medium,本人英语能力有限,配合原文更好理解

什么是反射?

大部分情况,GO语言中变量,类型,和函数都是非常简单的,当你需要一个类型时,你定义一个类型:

type Foo struct{
  A int
  B string
}

在你需要一个变量时,你定义一个变量:

var x Foo

或者你需要一个函数,你定义了一个函数:

func DoSomething(f Foo){
  fmt.Println(f.A, f.B)
}

有些时候,你在编写程序时并没有把数据初始给变量,但是在程序运行时要使用它。也许您正在尝试将文件或网络请求中的数据映射到变量中。也许你想构建一个适用于不同类型的工具。在这些情况下,您需要使用反射(Reflect)。Reflection使您能够在运行时检查类型。它还允许您在运行时检查,修改和创建变量,函数和结构.
Go语言中的反射,围绕三个概念构建:

  • Types
  • Kinds
  • Values

标准库中的Reflect Package包含了在GO语言中实现反射的主要类型和函数.

获取类型

首先让我们来看看类型。您可以使用反射来获取变量var的类型,函数调用varType:= reflect.TypeOf(var)。这将返回一个Reflection.Type类型的变量,该变量包含有关定义传入的变量的类型的各种信息的方法。
我们将看到的第一个方法是Name()。顾名思义,它返回类型的名称。某些类型(如切片或指针)没有名称,此方法返回空字符串。
下一个方法,在我看来,第一个非常有用的方法是Kind()
Kind是由Type构成的--slicemappointerstructinterfacestringarrayfunctionint或其他基本类型。KindType之间的差异可能很难理解,但是这样想。如果定义名为Foo的结构,则Kind为struct,Type为Foo。
使用反射时需要注意的一件事:反射包中的所有内容都假定您知道自己在做什么,如果使用不正确,许多函数和方法调用将会出现Panic。例如,如果您在reflect.Type上调用一个方法,该方法与当前类型不同的类型相关联,那么您的代码将会出现Panic。始终记得在使用反射类型时,哪些方法可行,哪些方法会出现Panic
如果变量是pointermapslicechannelarray,则可以使用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

你可以自己运行一下这个示例 https://play.golang.org/p/lZ97yAUHxX

创建一个新实例

除了检查变量的类型之外,您还可以使用反射来读取,设置或创建值。首先,您需要使用refVal := reflect.ValueOf(var)为变量创建一个reflect.Value实例。如果您希望能够使用反射来修改值,则必须使用refPtrVal := reflect.ValueOf(&var)来获取指向变量的指针;如果不这样做,您可以使用反射读取值,但不能修改它。
一旦有了reflect.Value,就可以使用Type()方法获得变量的reflect.Type
如果要修改值,请记住它必须是指针,并且必须首先取消引用指针。你使用refPtrVal.Elem()Set(newRefVal)进行更改,传递给Set()的值也必须是reflect.Value类型。
如果要创建新值,可以使用函数调用newPtrVal := reflect.New(varType),传入reflect.Type。这将返回一个您可以修改的指针值。
使用Elem().Set()来设置值,就像上边说的那样。
最后,您可以通过调用Interface()方法返回到正常变量。因为Go没有泛型,所以变量的原始类型丢失了;该方法返回interface{}类型的值。如果您创建了一个指针并且想修改指针的值,则需要使用Elem().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

你可以自己运行一下这个示例 https://play.golang.org/p/PFcEYfZqZ8

不适用Make来创建实例

除了创建内置和用户定义类型的实例之外,您还可以使用反射来创建通常需要make函数来创建的实例。您可以使用reflect.MakeSlicereflect.MakeMapreflect.MakeChan函数制作slicemapchannel
在所有情况下,您通过提供一个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]

你可以自己运行一下这个示例 https://play.golang.org/p/z4tnyEf6bH

创建函数

Reflection不仅可以让您创建存储数据的变量。您也可以使用reflect.MakeFunc来创建新函数。此函数需要提供两个参数

  1. 函数的类型reflect.Type
  2. 包含输入参数的类型[]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

你可以自己运行一下这个示例 https://play.golang.org/p/QZ8ttFZzGx

我想要一个新的结构体

使用Go中的反射还可以做一件事。您可以通过将一系列reflect.StructField实例传递给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]

你可以自己运行一下这个示例 https://play.golang.org/p/lJiTP6vYYN

什么不能做?

反射有一个很大的局限。虽然您可以使用反射来创建新的函数,但是无法在运行时为结构体创建新方法,这意味着您无法在运行时使用反射来实现接口。这也意味着使用反射来创建一个新结构体,可能会以奇怪的方式破坏。
我们快速回顾一下委托机制,大多数情况下,当你在一个结构中有一个字段时,你给它一个名字。在这个例子中,我们有两种类型,FooBar

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) // 传递一个Foo的实例,它符合接口,所以没有问题
	DoDouble(b) // 传递一个Bar的实例;它也没有问题
}

如果您在https://play.golang.org/p/aeroNQ7bEI上运行此代码,您将看到两件事。首先,Bar中的Foo字段没有名称。这使它成为一个匿名或嵌入的域。其次,Bar被视为满足Doubler接口,即使Double方法仅由Foo实现。这种能力称为授权;在编译时,Go会自动在Bar上生成与Foo上的方法匹配的方法。这不是继承;如果您尝试将Bar传递给需要Foo的函数,则代码将无法编译.
但是,如果您使用反射来构建具有嵌入字段的结构体,并且您尝试访问这些字段上的方法,则可能会出现一些非常奇怪的行为。最好的办法是远离使用它们。
Go GitHub存储库中存在修复此https://github.com/golang/go/issues/15924的问题,另一个问题是要求使用一套方法来定义新类型https://github.com/golang/go/issues/16522
不幸的是,这两个问题暂时没有任何进展。
这有什么用?如果我们可以动态实现接口,我们可以做什么?好吧,就像我们能够通过利用Go支持生成函数来为函数生成包装器一样,我们也可以为接口做同样的事情。在Java中,此功能称为动态代理。与注释结合使用时,它提供了一种从命令式编程风格转变为声明性编程风格的强大方法。一个很好的例子是JDBI。它是一个Java库,允许您通过定义使用SQL查询注释的接口来构建DAO层。通常为支持数据库交互而编写的所有样板代码都是在运行时动态生成的。那很厉害!

weinxin
我的微信
欢迎来撩!!

发表评论

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen: