Go 反射入门

Posted by 渐行渐远 on Monday, April 19, 2021 共4630字

鸡汤:~

自己无论多么努力,似乎都得不到社会的进一步认可;相反,如果按部就班地做事情,好像也坏不到哪里去。冥冥之中似乎被这两条线给框死,其实这就是命。

针对文章中代码的行号,我都在试图和源码保持一致,但源码也在不停地迭代,无法做到 100% 一致,主要还是看一个参考,了解一个脉络。

Indirect

通过下面的例子,我们来深入了解 reflect 数据包。我们定义 People 的结构体类型,来开始探索。

我们先来熟悉一下 reflect.Indirect 方法的使用。reflect.Indirect 返回的是底层的值类型,所以,下面第 12 行代码中调用 Kind 返回的类型为 struct。 而第 10 行代码输出结构体本身的 Kind 类型为 ptr

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
type People struct {
    Age int64
}

func main() {
    p := &People{
        Age: 30,
    }

  	fmt.Println(reflect.ValueOf(p).Kind())		// ouput: ptr
    value := reflect.Indirect(reflect.ValueOf(p))
  	fmt.Println(value.Kind())		// output: struct
}

Indriect 英文的含义形容词 “间接的”。这算的上是 reflect 包中比较简单的方法了。间接和直接的区别在于是否使用到指针,因为 Indirect 返回的是指针指向的值。如果参数本身不是指针类型,那就不需要做额外处理。

2339
2340
2341
2342
2343
2344
2345
2346
2347
// 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() != Ptr {
		return v
	}
	return v.Elem()
}

在哪里会用到 Indirect 方法呢?主要是用来操作底层的数据结构。常见的就是结构体,如果入参是一个指针,我们只有获取到底层的结构体,才可以应用结构体的各种操作方法。 诸如,获取结构体的值、标签等。

Kind

我认为它算 reflect 包的灵魂之一了,虽然本质上只是 uint 的别名,但它本身的信息却足够巨大。整个 Go 的基础数据类型都在这里了。工程代码中我们会声明各种类型的结构体, 比如示例代码中的 People 类型,它的 Kind 类型就是 Struct

230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
// A Kind represents the specific kind of type that a Type represents.
// The zero Kind is not a valid kind.
type Kind uint

const (
	Invalid Kind = iota
	Bool
	Int
	Int8
	Int16
	Int32
	Int64
	Uint
	Uint8
	Uint16
	Uint32
	Uint64
	Uintptr
	Float32
	Float64
	Complex64
	Complex128
	Array
	Chan
	Func
	Interface
	Map
	Ptr
	Slice
	String
	Struct
	UnsafePointer
)

知道这些有什么用呢?关键的一点,判断入参的类型,究竟是结构体、还是指针、或者别的类型。在框架类的代码中,经常有这种类型判断。简单举例说明一下:

本质上讲,Go 语言中不外乎这两种定义类型的方式。

1
2
type IString string
type YString = string

IString 算是重新声明一个新的类型,它和 string 必须强制转换。YString 是类型的一个别名, 本身和 string 类型就是完全相同的。项目代码中,其实存在很多像 IString 这样的类型,最常见的就是结构体声明。

如果我们直接通过安全断言的方式来判断类型的话, 势必要写很多的条件语句,而通过 Kind 就不需要这么麻烦。 因为自定义的类型是无限的,而底层的数据类型是有限的。

Type

反射的 ValueOfTypeOf 函数是使用反射的入口,分别对应于接口类型的具体值和动态类型。下面的例子中, 第 5 行代码调用 People 对象 ValueType 和第 6 行代码直接获取的 Type 做比较,输出值是相同的。

1
2
3
4
5
6
7
8
9
	p := &People{
        Age: 30,
    }

    valueType := reflect.ValueOf(p).Type()
    dynamicType := reflect.TypeOf(p)
    if valueType == dynamicType {
      	fmt.Println("equal")		// output: equal
    }

一些博客文章介绍了反射类型的转换关系,ValueTypeinterface 三种类型之间相互转换。这个例子侧面说明了 ValueType 的转换,但在源码的实现上, 通过调用 Value 类型的 Type 方法和直接使用 TypeOf 差别其实挺大的。

如果对底层的实现细节不清楚,这个转换关系其实挺让人困惑的。这究竟是一个"恒等式",还是只针对某些具体的类型,这样的转换才成立。我其实就特别困惑, 我不仅仅是困惑 Value 类型到 Type 类型的转换,还困惑通过直接调用 TypeOf 获取 Type 和通过调用 ValueOf 获取 Type 两者的效率差异。

TypeOf

TypeOf的逻辑比较简单,通过使用 unsafe 包将接口类型强转一个 emptyInterface 类型,然后直接返回 emptyInterface 类型的 typ 属性。

1366
1367
1368
1369
1370
1371
// TypeOf returns the reflection Type that represents the dynamic type of i.
// If i is a nil interface value, TypeOf returns nil.
func TypeOf(i interface{}) Type {
	eface := *(*emptyInterface)(unsafe.Pointer(&i))
	return toType(eface.typ)
}

TypeOf 函数很简单,我们只需要查看 emptyInterface 的类型定义,然后查看这个类型的 typ 属性来确认 Type。另一个细节就是 toType 方法, 也需要进去函数看一看具体的实现。

interface 被大家分成两类来区分,空接口和非空接口,下面看到的就是空接口 emptyInterface 的类型声明。typ 指向具体的类型,而 word 指向具体值的地址。 我们也可以看出,默认将某一个具体类型转换成 interface 类型,系统还是做了很多转换工作的。

193
194
195
196
197
// emptyInterface is the header for an interface{} value.
type emptyInterface struct {
	typ  *rtype
	word unsafe.Pointer
}

深入 toType 方法看一下源码实现,注释其实挺丰富的,我却只能看懂字面意思。但从功能上讲,代码的作用很简单,当入参为 nil 时,直接返回 nil。 因为返回值类型 Type 是一个接口类型,针对接口类型来说,返回 *rtype 类型的 nilnil 可完全是两码事。

2968
2969
2970
2971
2972
2973
2974
2975
2976
2977
2978
// toType converts from a *rtype to a Type that can be returned
// to the client of package reflect. In gc, the only concern is that
// a nil *rtype must be replaced by a nil Type, but in gccgo this
// function takes care of ensuring that multiple *rtype for the same
// type are coalesced into a single Type.
func toType(t *rtype) Type {
	if t == nil {
		return nil
	}
	return t
}

ValueOf

ValueOf 的源码如下,更多的细节还是需要查看 unpackEface 方法。返回值类型 ValueType 不同,Value 是一个结构体值类型, 而 Type 是一个接口类型。当入参是一个 nil 时,直接返回一个初始化的 Value 对象。

2349
2350
2351
2352
2353
2354
2355
2356
2357
2358
2359
2360
2361
2362
2363
// ValueOf returns a new Value initialized to the concrete value
// stored in the interface i. ValueOf(nil) returns the zero Value.
func ValueOf(i interface{}) Value {
	if i == nil {
		return Value{}
	}

	// TODO: Maybe allow contents of a Value to live on the stack.
	// For now we make the contents always escape to the heap. It
	// makes life easier in a few places (see chanrecv/mapassign
	// comment below).
	escapes(i)

	return unpackEface(i)
}

下面是 unpackEface 的方法实现,通过第 13 行返回值,我们可以看出,Value 操作的也是 emptyInterface 结构体,只是再它的基础上, 多了一个 flag 属性。

对比之前 TypeOf 的源码,TypeOf 仅仅返回 emptyInterfacetyp 属性,也就是底层的数据类型。 而 ValueOfTypeOf 的基础上还返回了 emptyInterfaceword 属性,并且设置了一个 flag

正因为如此,Value 可以获取 TypeType 类型却无法获取 Value。在使用反射遍历结构体类型的时候,如果想要获取结构体字段的值,就必须操作 Value 对象了。

141
142
143
144
145
146
147
148
149
150
151
152
153
154
// unpackEface converts the empty interface i to a Value.
func unpackEface(i interface{}) Value {
	e := (*emptyInterface)(unsafe.Pointer(&i))
	// NOTE: don't read e.word until we know whether it is really a pointer or not.
	t := e.typ
	if t == nil {
		return Value{}
	}
	f := flag(t.Kind())
	if ifaceIndir(t) {
		f |= flagIndir
	}
	return Value{t, e.word, f}
}

我其实并不想把 Value 结构体的源码复制到这里,因为它的注释太长了。我就非常抵触代码的行数特别多(包括注释),因为不利于阅读。 但最终还是整体复制过来了,说服自己的就一个观点,既然是复制,就要跟原来的保持一致。

Value 的结构体声明没有什么特别,除了 flag 属性是我们自己设置的,其他都是对象转换成 emptyInterface 后直接获取的。

16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
// Value is the reflection interface to a Go value.
//
// Not all methods apply to all kinds of values. Restrictions,
// if any, are noted in the documentation for each method.
// Use the Kind method to find out the kind of value before
// calling kind-specific methods. Calling a method
// inappropriate to the kind of type causes a run time panic.
//
// The zero Value represents no value.
// Its IsValid method returns false, its Kind method returns Invalid,
// its String method returns "<invalid Value>", and all other methods panic.
// Most functions and methods never return an invalid value.
// If one does, its documentation states the conditions explicitly.
//
// A Value can be used concurrently by multiple goroutines provided that
// the underlying Go value can be used concurrently for the equivalent
// direct operations.
//
// To compare two Values, compare the results of the Interface method.
// Using == on two Values does not compare the underlying values
// they represent.
type Value struct {
	// typ holds the type of the value represented by a Value.
	typ *rtype

	// Pointer-valued data or, if flagIndir is set, pointer to data.
	// Valid when either flagIndir is set or typ.pointers() is true.
	ptr unsafe.Pointer

	// flag holds metadata about the value.
	// The lowest bits are flag bits:
	//	- flagStickyRO: obtained via unexported not embedded field, so read-only
	//	- flagEmbedRO: obtained via unexported embedded field, so read-only
	//	- flagIndir: val holds a pointer to the data
	//	- flagAddr: v.CanAddr is true (implies flagIndir)
	//	- flagMethod: v is a method value.
	// The next five bits give the Kind of the value.
	// This repeats typ.Kind() except for method values.
	// The remaining 23+ bits give a method number for method values.
	// If flag.kind() != Func, code can assume that flagMethod is unset.
	// If ifaceIndir(typ), code can assume that flagIndir is set.
	flag

	// A method value represents a curried method invocation
	// like r.Read for some receiver r. The typ+val+flag bits describe
	// the receiver r, but the flag's Kind bits say Func (methods are
	// functions), and the top bits of the flag give the method number
	// in r's type's method table.
}

type flag uintptr

我们开始步入主题,查看 Value 类型的 Type 方法。一般的情况,方法体会在 1912 行直接返回,后续的其他逻辑并不会执行,正如注释所说,这是 Easy case 的情况。 如果我们不在反射的基础是使用反射,后续的逻辑基本上是触发不到的。即使声明一个函数或者方法,然后对他们调用 ValueOfTypeOf 方法也是一样的。

1904
1905
1906
1907
1908
1909
1910
1911
1912
1913
1914
1915
1916
1917
1918
1919
1920
1921
1922
1923
1924
1925
1926
1927
1928
1929
1930
1931
1932
1933
1934
1935
// Type returns v's type.
func (v Value) Type() Type {
	f := v.flag
	if f == 0 {
		panic(&ValueError{"reflect.Value.Type", Invalid})
	}
	if f&flagMethod == 0 {
		// Easy case
		return v.typ
	}

	// Method value.
	// v.typ describes the receiver, not the method type.
  // 获取方法的下标
	i := int(v.flag) >> flagMethodShift
	if v.typ.Kind() == Interface {
		// Method on interface.
		tt := (*interfaceType)(unsafe.Pointer(v.typ))
		if uint(i) >= uint(len(tt.methods)) {
			panic("reflect: internal error: invalid method index")
		}
		m := &tt.methods[i]
		return v.typ.typeOff(m.typ)
	}
	// Method on concrete type.
	ms := v.typ.exportedMethods()
	if uint(i) >= uint(len(ms)) {
		panic("reflect: internal error: invalid method index")
	}
	m := ms[i]
	return v.typ.typeOff(m.mtyp)
}

在这里就有疑问了,Easy case 后续的逻辑什么情况下才会被触发呢?flagMethod 是如何设置到 v.flag 上的呢(在 unpackEface 方法中我们可没有看到过这个标志位)?

ValueOf 方法中并不会把 flag 属性的 flagMethod 标识位设置为1。下面是标志位的具体位置。flagMethod 为把 1 左移 9 位,从右到左,第 10 给比特位为 1。 但是 flag 的第 10 个比特位并不是 1,falg 所有为 1 的比特位和 falgMethod 与运算后都是 0 。

68
69
70
71
72
73
74
75
76
77
78
const (
	flagKindWidth        = 5 // there are 27 kinds
	flagKindMask    flag = 1<<flagKindWidth - 1
	flagStickyRO    flag = 1 << 5
	flagEmbedRO     flag = 1 << 6
	flagIndir       flag = 1 << 7
	flagAddr        flag = 1 << 8
	flagMethod      flag = 1 << 9
	flagMethodShift      = 10
	flagRO          flag = flagStickyRO | flagEmbedRO
)

通过全局搜索 flagMethod 关键字,可以隐约猜测到,触发后续逻辑的应该是一个方法的 Value,而且这个方法的 Value 还必须从结构体的 Value中获得。 我们给 People 对象追加一个方法, 代码第 10 行获取方法的 Value 对象,然后通过这个对象获取 Type

通过断点调试,这样处理后,确实触发了Easy case之后的逻辑。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
func (p *People) Describe() {
    fmt.Println("people age:", p.Age)
}

func main() {
    p := &People{
        Age: 30,
    }

    methodType := reflect.ValueOf(p).MethodByName("Describe").Type()
    fmt.Println(methodType)
}

MethodByName

这是基于上述 flag 内容的延伸。那么,MethodByName 具体又做了什么操作呢?看第 10 行的代码,flagMethod 标志位的含义就明白了, 它标志是否是方法的 Valuev 必须不能被设置 flagMethod 标志,如果设置的话,会返回空的 Value

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
// MethodByName returns a function value corresponding to the method
// of v with the given name.
// The arguments to a Call on the returned function should not include
// a receiver; the returned function will always use v as the receiver.
// It returns the zero Value if no method was found.
func (v Value) MethodByName(name string) Value {
	if v.typ == nil {
		panic(&ValueError{"reflect.Value.MethodByName", Invalid})
	}
	if v.flag&flagMethod != 0 {
		return Value{}
	}
	m, ok := v.typ.MethodByName(name)
	if !ok {
		return Value{}
	}
	return v.Method(m.Index)
}

跳过 MethodByName 的方法实现,见第 10 行代码,我们找到了设置 flagMethod 的地方。

1316
1317
1318
1319
1320
1321
1322
1323
1324
1325
1326
1327
1328
1329
1330
1331
1332
1333
1334
// Method returns a function value corresponding to v's i'th method.
// The arguments to a Call on the returned function should not include
// a receiver; the returned function will always use v as the receiver.
// Method panics if i is out of range or if v is a nil interface value.
func (v Value) Method(i int) Value {
	if v.typ == nil {
		panic(&ValueError{"reflect.Value.Method", Invalid})
	}
	if v.flag&flagMethod != 0 || uint(i) >= uint(v.typ.NumMethod()) {
		panic("reflect: Method index out of range")
	}
	if v.typ.Kind() == Interface && v.IsNil() {
		panic("reflect: Method on nil interface value")
	}
	fl := v.flag.ro() | (v.flag & flagIndir)
	fl |= flag(Func)
	fl |= flag(i)<<flagMethodShift | flagMethod
	return Value{v.typ, v.ptr, fl}
}

方法

方法的反射使用的比较少,但很多情况非常有用。我们可以通过指定一个方法名称来查找某个类型中是否存在该方法,如果存在的话,我们还可以通过反射来实现调用。 通俗的说,我们可以通过 HTTP 协议传递一个方法名,最终让服务端执行这个方法。这也是最基本的用法了

下面例子中的第10行代码阐述了,通过名字来查找函数并传递参数进行调用。在 Go 的函数调用都是通过反射的数组形式,入参列表和出参列表都是一个 Value 类型的数组。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
	type PreFunc struct {
	}
	
	func (f PreFunc) DoName(ctx context.Context) {
		fmt.Println("Output: DoName")
	}
	
	func main() {
		p := PreFunc{}
		reflect.ValueOf(p).MethodByName("DoName").Call([]reflect.Value{
		reflect.ValueOf(context.TODO())})
	}

通过 reflect 包我们可以获取到方法的入参个数、出参个数,执行方法、获取方法返回值、在运行时实现方法等操作。观察一些 RPC 的客户端也是这样实现的。 我们只需要在结构体中声明方法类型的字段,在函数调用的时候将它实现。

我们声明了结构体中字段 PrintName 为方法类型,然后在 main 方法中将它实例化了,最后,直接调用这个方法。这是对原理的简化版例子,如果我们把声明函数的入参都当做 HTTP 的请求参数, 然后统一编码发送给服务端,就是一套统一的 RPC 实现。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
	type Live struct {
		PrintName func(ctx context.Context)
	}
	
	func main() {
		p := &Live{}
		vp := reflect.ValueOf(p).Elem()
	
		f := vp.FieldByName("PrintName")
		f.Set(reflect.MakeFunc(f.Type(), func(args []reflect.Value) (results []reflect.Value) {
			ctx := args[0].Interface().(context.Context)
			fmt.Println("观看直播", ctx.Value("uid"))
			return nil
		}))
	
		ctx := context.TODO()
		ctx = context.WithValue(ctx, "uid", "1031101")
		p.PrintName(ctx)
	}

Go 函数一般都返回两个参数,其中一个是 error 类型,例子中声明的 PrintName 方法没有返回值,所以简单了不少。如果我们要返回一个 nil 的 error,我们要如何构造这个 nil 的 error 的 reflect.Value 类型呢?

像下面这样声明变量可以吗?看 panic 信息返回的是一个没有类型的零值。所以说,ValueOf 之后是没有类型信息的。

// panic: reflect: function created by MakeFunc using closure returned zero Value
var nilError reflect.Value = reflect.ValueOf((*error)(nil)).Elem()

这样声明零值可以正常返回

var nilError reflect.Value = reflect.Zero(reflect.TypeOf((*error)(nil)).Elem())
// 或则更换一种写法
var nilError reflect.Value = reflect.Zero(reflect.ValueOf((*error)(nil)).Type().Elem())