解密 Go 语言之反射 Reflect

开发 后端
在所有的语言中,反射这一功能基本属于必不可少的模块。虽说 “反射” 这个词让人根深蒂固,但更多的还是 WHY。反射到底是什么,反射又是基于什么法则实现的?

[[361315]]

本文转载自微信公众号「脑子进煎鱼了」,作者陈煎鱼 。转载本文请联系脑子进煎鱼了公众号。

 大家好,我是煎鱼。今天是 2020 年的最后一天,让我们一起继续愉快的学习吧 :)。

在所有的语言中,反射这一功能基本属于必不可少的模块。

虽说 “反射” 这个词让人根深蒂固,但更多的还是 WHY。反射到底是什么,反射又是基于什么法则实现的?

今天我们通过这篇文章来一一揭晓,以 Go 语言为例,了解反射到底为何物,其底层又是如何实现的。

反射是什么

在计算机学中,反射是指计算机程序在运行时(runtime)可以访问、检测和修改它本身状态或行为的一种能力。

用比喻来说,反射就是程序在运行的时候能够 “观察” 并且修改自己的行为(来自维基百科)。

简单来讲就是,应用程序能够在运行时观察到变量的值,并且能够修改他。

一个例子

最常见的 reflect 标准库例子,如下:

  1. import ( 
  2.  "fmt" 
  3.  "reflect" 
  4.  
  5. func main() { 
  6.  rv := []interface{}{"hi", 42, func() {}} 
  7.  for _, v := range rv { 
  8.   switch v := reflect.ValueOf(v); v.Kind() { 
  9.   case reflect.String: 
  10.    fmt.Println(v.String()) 
  11.   case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: 
  12.    fmt.Println(v.Int()) 
  13.   default
  14.    fmt.Printf("unhandled kind %s", v.Kind()) 
  15.   } 
  16.  } 

输出结果:

  1. hi 
  2. 42 
  3. unhandled kind func 

在程序中主要是声明了 rv 变量,变量类型为 interface{},其包含 3 个不同类型的值,分别是字符串、数字、闭包。

而在使用 interface{} 时常见于不知道入参者具体的基本类型是什么,那么我们就会用 interface{} 类型来做一个伪 “泛型”。

此时又会引出一个新的问题,既然入参是 interface{},那么出参时呢?

Go 语言是强类型语言,入参是 interface{},出参也肯定是跑不了的,因此必然离不开类型的判断,这时候就要用到反射,也就是 reflect 标准库。反射过后又再进行 (type) 的类型断言。

这就是我们在编写程序时最常遇见的一个反射使用场景。

Go reflect

reflect 标准库中,最核心的莫过于 reflect.Type 和 reflect.Value 类型。而在反射中所使用的方法都围绕着这两者进行,其方法主要含义如下:

  • TypeOf 方法:用于提取入参值的类型信息。
  • ValueOf 方法:用于提取存储的变量的值信息。

reflect.TypeOf

演示程序:

  1. func main() { 
  2.  blog := Blog{"煎鱼"
  3.  typeof := reflect.TypeOf(blog) 
  4.  fmt.Println(typeof.String()) 

输出结果:

  1. main.Blog 

从输出结果中,可得出 reflect.TypeOf 成功解析出 blog 变量的类型是 main.Blog,也就是连 package 都知道了。

通过人识别的角度来看似乎很正常,但程序就不是这样了。他是怎么知道 “他” 是哪个 package 下的什么呢?

我们一起追一下源码看看:

  1. func TypeOf(i interface{}) Type { 
  2.  eface := *(*emptyInterface)(unsafe.Pointer(&i)) 
  3.  return toType(eface.typ) 

从源码层面来看,TypeOf 方法中主要涉及三块操作,分别如下:

  1. 使用 unsafe.Pointer 方法获取任意类型且可寻址的指针值。
  2. 利用 emptyInterface 类型进行强制的 interface 类型转换。
  3. 调用 toType 方法转换为可供外部使用的 Type 类型。

而这之中信息量最大的是 emptyInterface 结构体中的 rtype 类型:

  1. type rtype struct { 
  2.  size       uintptr 
  3.  ptrdata    uintptr  
  4.  hash       uint32  
  5.  tflag      tflag  
  6.  align      uint8   
  7.  fieldAlign uint8   
  8.  kind       uint8    
  9.  equal     func(unsafe.Pointer, unsafe.Pointer) bool 
  10.  gcdata    *byte   
  11.  str       nameOff  
  12.  ptrToThis typeOff  

在使用上最重要的是 rtype 类型,其实现了 Type 类型的所有接口方法,因此他可以直接作为 Type 类型返回。

而 Type 本质上是一个接口实现,其包含了获取一个类型所必要的所有方法:

  1. type Type interface { 
  2.  // 适用于所有类型 
  3.  // 返回该类型内存对齐后所占用的字节数 
  4.  Align() int 
  5.  
  6.  // 仅作用于 strcut 类型 
  7.  // 返回该类型内存对齐后所占用的字节数 
  8.  FieldAlign() int 
  9.  
  10.  // 返回该类型的方法集中的第 i 个方法 
  11.  Method(int) Method 
  12.  
  13.  // 根据方法名获取对应方法集中的方法 
  14.  MethodByName(string) (Method, bool) 
  15.  
  16.  // 返回该类型的方法集中导出的方法的数量。 
  17.  NumMethod() int 
  18.  
  19.  // 返回该类型的名称 
  20.  Name() string 
  21.  ... 

建议大致过一遍,了解清楚有哪些方法,再针对向看就好。

主体思想是给自己大脑建立一个索引,便于后续快速到 pkg.go.dev 上查询即可。

reflect.ValueOf

演示程序:

  1. func main() { 
  2.  var x float64 = 3.4 
  3.  fmt.Println("value:", reflect.ValueOf(x)) 

输出结果:

  1. value: 3.4 

从输出结果中,可得知通过 reflect.ValueOf 成功获取到了变量 x 的值为 3.4。与 reflect.TypeOf 形成一个相匹配,一个负责获取类型,一个负责获取值。

那么 reflect.ValueOf 是怎么获取到值的呢,核心源码如下:

  1. func ValueOf(i interface{}) Value { 
  2.  if i == nil { 
  3.   return Value{} 
  4.  } 
  5.  
  6.  escapes(i) 
  7.  
  8.  return unpackEface(i) 
  9.  
  10. func unpackEface(i interface{}) Value { 
  11.  e := (*emptyInterface)(unsafe.Pointer(&i)) 
  12.  t := e.typ 
  13.  if t == nil { 
  14.   return Value{} 
  15.  } 
  16.  f := flag(t.Kind()) 
  17.  if ifaceIndir(t) { 
  18.   f |= flagIndir 
  19.  } 
  20.  return Value{t, e.word, f} 

从源码层面来看,ValueOf 方法中主要涉及如下几个操作:

  1. 调用 escapes 让变量 i 逃逸到堆上。
  2. 将变量 i 强制转换为 emptyInterface 类型。
  3. 将所需的信息(其中包含值的具体类型和指针)组装成 reflect.Value 类型后返回。

何时类型转换

在调用 reflect 进行一系列反射行为时,Go 又是在什么时候进行的类型转换呢?

毕竟我们传入的是 float64,而函数如参数是 inetrface 类型。

查看汇编如下:

  1. $ go tool compile -S main.go                          
  2.  ... 
  3.  0x0058 00088 ($GOROOT/src/reflect/value.go:2817) LEAQ type.float64(SB), CX 
  4.  0x005f 00095 ($GOROOT/src/reflect/value.go:2817) MOVQ CX, reflect.dummy+8(SB) 
  5.  0x0066 00102 ($GOROOT/src/reflect/value.go:2817) PCDATA $0, $-2 
  6.  0x0066 00102 ($GOROOT/src/reflect/value.go:2817) CMPL runtime.writeBarrier(SB), $0 
  7.  0x006d 00109 ($GOROOT/src/reflect/value.go:2817) JNE 357 
  8.  0x0073 00115 ($GOROOT/src/reflect/value.go:2817) MOVQ AX, reflect.dummy+16(SB) 
  9.  0x007a 00122 ($GOROOT/src/reflect/value.go:2348) PCDATA $0, $-1 
  10.  0x007a 00122 ($GOROOT/src/reflect/value.go:2348) MOVQ CX, reflect.i+64(SP) 
  11.  0x007f 00127 ($GOROOT/src/reflect/value.go:2348) MOVQ AX, reflect.i+72(SP) 
  12.  ... 

显然,Go 语言会在编译阶段就会完成分析,且进行类型转换。这样子 reflect 真正所使用的就是 interface 类型了。

reflect.Set

演示程序:

  1. func main() { 
  2.  i := 2.33 
  3.  v := reflect.ValueOf(&i) 
  4.  v.Elem().SetFloat(6.66) 
  5.  log.Println("value: ", i) 

输出结果:

  1. value:  6.66 

从输出结果中,我们可得知在调用 reflect.ValueOf 方法后,我们利用 SetFloat 方法进行了值变更。

核心的方法之一就是 Setter 相关的方法,我们可以一起看看其源码是怎么实现的:

  1. func (v Value) Set(x Value) { 
  2.  v.mustBeAssignable() 
  3.  x.mustBeExported() // do not let unexported x leak 
  4.  var target unsafe.Pointer 
  5.  if v.kind() == Interface { 
  6.   target = v.ptr 
  7.  } 
  8.  x = x.assignTo("reflect.Set", v.typ, target) 
  9.  if x.flag&flagIndir != 0 { 
  10.   typedmemmove(v.typ, v.ptr, x.ptr) 
  11.  } else { 
  12.   *(*unsafe.Pointer)(v.ptr) = x.ptr 
  13.  } 
  1. 检查反射对象及其字段是否可以被设置。
  2. 检查反射对象及其字段是否导出(对外公开)。
  3. 调用 assignTo 方法创建一个新的反射对象并对原本的反射对象进行覆盖。
  4. 根据 assignTo 方法所返回的指针值,对当前反射对象的指针进行值的修改。

简单来讲就是,检查是否可以设置,接着创建一个新的对象,最后对其修改。是一个非常标准的赋值流程。

反射三大定律

Go 语言中的反射,其归根究底都是在实现三大定律:

  1. Reflection goes from interface value to reflection object.
  2. Reflection goes from reflection object to interface value.
  3. To modify a reflection object, the value must be settable.

我们将针对这核心的三大定律进行介绍和说明,以此来理解 Go 反射里的各种方法是基于什么理念实现的。

第一定律

反射的第一定律是:“反射可以从接口值(interface)得到反射对象”。

示例代码:

  1. func main() { 
  2.  var x float64 = 3.4 
  3.  fmt.Println("type:", reflect.TypeOf(x)) 

输出结果:

  1. type: float64 

可能有读者就迷糊了,我明明在代码中传入的变量 x,他的类型是 float64。怎么就成从接口值得到反射对象了。

其实不然,虽然在代码中我们所传入的变量基本类型是 float64,但是 reflect.TypeOf 方法入参是 interface{},本质上 Go 语言内部对其是做了类型转换的。这一块会在后面会进一步展开说明。

第二定律

反射的第二定律是:“可以从反射对象得到接口值(interface)”。其与第一条定律是相反的定律,可以是互相补充了。

示例代码:

  1. func main() { 
  2.  vo := reflect.ValueOf(3.4) 
  3.  vf := vo.Interface().(float64) 
  4.  log.Println("value:", vf) 

输出结果:

  1. value: 3.4 

可以看到在示例代码中,变量 vo 已经是反射对象,然后我们可以利用其所提供的的 Interface 方法获取到接口值(interface),并最后强制转换回我们原始的变量类型。

第三定律

反射的第三定律是:“要修改反射对象,该值必须可以修改”。第三条定律看上去与第一、第二条均无直接关联,但却是必不可少的,因为反射在工程实践中,目的一就是可以获取到值和类型,其二就是要能够修改他的值。

否则反射出来只能看,不能动,就会造成这个反射很鸡肋。例如:应用程序中的配置热更新,必然会涉及配置项相关的变量变动,大多会使用到反射来变动初始值。

示例代码:

  1. func main() { 
  2.  i := 2.33 
  3.  v := reflect.ValueOf(&i) 
  4.  v.Elem().SetFloat(6.66) 
  5.  log.Println("value: ", i) 

输出结果:

  1. value:  6.66 

单从结果来看,变量 i 的值确实从 2.33 变成了 6.66,似乎非常完美。

但是单看代码,似乎有些 “问题”,怎么设置一个反射值这么 ”麻烦“:

为什么必须传入变量 i 的指针引用?

为什么变量 v 在设置前还需要 Elem 一下?

本叛逆的 Gophper 表示我就不这么设置,行不行呢,会不会出现什么问题:

  1. func main() { 
  2.  i := 2.33 
  3.  reflect.ValueOf(i).SetFloat(6.66) 
  4.  log.Println("value: ", i) 

报错信息:

  1. panic: reflect: reflect.Value.SetFloat using unaddressable value 
  2.  
  3. goroutine 1 [running]: 
  4. reflect.flag.mustBeAssignableSlow(0x8e) 
  5.         /usr/local/Cellar/go/1.15/libexec/src/reflect/value.go:259 +0x138 
  6. reflect.flag.mustBeAssignable(...) 
  7.         /usr/local/Cellar/go/1.15/libexec/src/reflect/value.go:246 
  8. reflect.Value.SetFloat(0x10b2980, 0xc00001a0b0, 0x8e, 0x401aa3d70a3d70a4) 
  9.         /usr/local/Cellar/go/1.15/libexec/src/reflect/value.go:1609 +0x37 
  10. main.main() 
  11.         /Users/eddycjy/go-application/awesomeProject/main.go:10 +0xc5 

根据上述提示可知,由于使用 “使用不可寻址的值”,因此示例程序无法正常的运作下去。并且这是一个 reflect 标准库本身就加以防范了的硬性要求。

这么做的原因在于,Go 语言的函数调用的传递都是值拷贝的,因此若不传指针引用,单纯值传递,那么肯定是无法变动反射对象的源值的。因此 Go 标准库就对其进行了逻辑判断,避免出现问题。

因此期望变更反射对象的源值时,我们必须主动传入对应变量的指针引用,并且调用 reflect 标准库的 Elem 方法来获取指针所指向的源变量,并且最后调用 Set 相关方法来进行设置。

总结

通过本文我们学习并了解了 Go 反射是如何使用,又是基于什么定律设计的。另外我们稍加关注,不难发现 Go 的反射都是基于接口(interface)来实现的,更进一步来讲,Go 语言中运行时的功能很多都是基于接口来实现的。

整体来讲,Go 反射是围绕着三者进行的,分别是 Type、Value 以及 Interface,三者相辅相成,而反射本质上与 Interface 存在直接关系,Interface 这一块的内容我们也将在后续的文章进行进一步的剖析。

 

责任编辑:武晓燕 来源: 脑子进煎鱼了
相关推荐

2021-05-12 08:53:54

Go语言调度

2021-10-23 06:42:14

Go语言接口

2021-10-03 22:18:14

Go语言整数

2022-03-28 13:34:26

Go泛型部署泛型

2021-10-09 07:52:01

Go程序重命名

2021-10-16 17:53:35

Go函数编程

2011-04-01 14:50:56

Java的反射机制

2024-01-05 20:46:14

2013-08-20 10:11:20

Go系统管理员

2021-10-18 10:53:26

Go 代码技术

2021-04-30 09:04:11

Go 语言结构体type

2024-01-08 08:23:07

Go语言代码

2013-07-10 11:11:05

PythonGo语言

2017-06-14 09:37:05

R语言Apriori算法

2012-02-13 10:03:31

编程开发

2018-08-01 15:10:02

GolangPython语言

2012-08-13 14:13:46

2020-09-24 10:50:53

加密解密语言hmac

2021-01-26 05:19:56

语言Go Context

2020-12-16 08:07:28

语言基础反射
点赞
收藏

51CTO技术栈公众号