写点什么

Go 反射的三大法则

作者:linlh
  • 2022 年 2 月 19 日
  • 本文字数:4181 字

    阅读完需:约 14 分钟

Go反射的三大法则

本文翻译自:https://go.dev/blog/laws-of-reflection,有删改。

核心要点

Go 语言的三大法则提供了对变量的便捷操作,使得可以实现更多的功能。核心要点在于 go 的变量和反射对象之间的转换,相当于两个不同的空间,可以类比于通信领域的时域和频域的区别,是一个东西的两种不同观察角度,通过反射可以获取变量在”反射域“中的信息,从而赋予程序更多的功能。


这篇文章主要是介绍了 Go 语言中反射的三大法则,官方的这篇文章中也给了一些示例代码,但是缺乏了在实际项目中对反射的应用,后续将补充下反射在实际项目中的应用。

Go types 和 interfaces

计算中的反射是程序检查自身结构的能力,特别是通过类型;它是元编程的一种形式。


Go 语言是强类型的,每个变量都有一个静态类型,也就是说在编译时就会确定一个已知并固定的类型。看下如下代码:


type MyInt int
var i intvar j MyInt
复制代码


那么 i 是 int 类型,j 是 MyInt 类型,这两个变量是不同的静态类型,尽管它们有相同的基础类型。它们不能不经过转换直接赋值给对方。


一个重要的类型是 interface 类型,代表了固定的方法集。


一个 interface 变量可以存储任意具体(non-interface)的值,只要这个值实现了 interface 的方法。一个比较明显的例子就是:io.Reader 和 io.Writer


type Reader interface {  Read(p []byte) (n int, err error)}
type Writer interface { Write(p []byte) (n int, err error)}
复制代码


任何一个类型只要实现了 Read 或者是 Write 方法带有这个签名的都可以说是实现了 io.Reader 或者 io.Writer。这意味着 io.Reader 类型,可以保存任意一个实现了 Read 方法的类型。


var r io.Readerr = os.Stdinr = bufio.NewReader(r)r = new(bytes.Buffer)
复制代码


有一点很重要,不管 r 存储了任何具体的值,r 的类型都是 io.Reader。


一个非常重要的 interface 类型的例子是空 interface,也就是 interface{} , 等同于别名:any, 它表示空的方法集,任何值都可以满足它,因为每个值都有零个或多个方法。


有些人说 Go 语言的 interface 是动态类型,这种说法是误导。它们都是静态类型,一个 interface 类型的变量一直都是一样的类型,尽管 interface 变量在运行时存储的内容变化可能导致类型变化,但是这个值一直都是满足这个 interface 的。

三大法则

1. 反射从 interface 值到 反射对象

基本功能上,反射仅仅是一种检查类型的机制和存储在 interface 变量中的值对。开始前需要知道两个类型 reflect 包中的 Type 和 Value。这两种类型允许访问 interface 变量的内容,还有两个简单的函数 reflect.TypeOf 和 reflect.ValueOf,从 interface 值中获取 reflecet.Type 和 reflect.Value。


package main
import ( "fmt" "reflect")
func main() { var x float64 = 3.4 fmt.Println("type:", reflect.TypeOf(x))}
复制代码


输出结果:


type: float64
复制代码


在这里你可能会奇怪这里没有 interface,而是将 float64 类型的变量 x 传给 reflect.TypeOf。但是 reflect.TypeOf 包含一个空的接口。当我们调用 reflect.TypeOf ,x 首先存储在一个 empty interface 中,然后作为参数传入。reflect.TypeOf 解出 empty interface 来恢复类型信息。


reflect.ValueOf 相应的恢复出 Value 了。


var x float64 = 3.4fmt.Println("Value:", reflect.ValueOf(x).String())
复制代码


输出结果:


value: <float64 Value>
复制代码


这里显式调用了 String 方法是因为默认 fmt 包会深入 reflect.Value 来显示具体的值,但是 String 方法不会。???


reflect.Type 和 reflect.Value 有很多的方法来检查和操作。一个重要的例子是 Value 有一个 Type 方法来返回 reflect.Value 的 Type。另外一个是 Type 和 Value 都有一个 Kind 方法返回一个常数表示它们存储的是什么项:Uint, Float64,Slice 等。Value 有 Int 和 Float 方法让我们来获取具体的值(对应的类型是 int64 和 float64)。


var x float64 = 3.4v := reflect.ValueOf(x)fmt.Println("type:", v.Type())fmt.Println("kind is float64:", v.Kind() == reflect.Float64)fmt.Println("value:", v.Float())
复制代码


输出结果:


type: float64kind is float64: truevalue: 3.4
复制代码


另外也有 SetInt 和 SetFloat 方法,但是使用它们之前我们需要理解 settability,这个是反射的第三法则


反射库有几个值得注意的属性。首先,为了保持 API 简单,Value 的 "getter" 和 “setter” 方法是在大的类型上存储值的,比如说 int64 用于存储所有有符号的整数。所以,Value 的 Int 方法返回类型是 int64,SetInt 设置值输入参数为 int64,当使用到得时候需要转化成实际的类型:


var x uint8 =- 'x'v := reflect.ValueOf(x)fmt.Println("type:", v.Type())fmt.Println("kind is uint8:", v.Kind() == reflect.Uint8)x = uinx8.(v.Uint()) // v.Uint() 返回的类型是 uint64
复制代码


第二个特性是说一个反射对象的 Kind 方法返回的基本类型,而不是静态类型,如果一个反射对象包含了用户定义的整数类型,那么 Kind 返回的还是 reflect.Int,尽管 x 的静态类型是 MyInt,而不是 int。也就是说,Kind 无法区分 int 和 MyInt,Type 可以。


type MyInt intvar x MyInt = 7v := reflect.ValueOf(x)fmt.Println("kind is Int:", v.Kind() == reflect.Int) // true
复制代码

2. 从反射对象到 interface 值

给定一个 reflect.Value,可以使用 Interface 方法恢复 interface value,实际上这个方法将 type 和 value 信息重新打包到 interface 的表示中返回该结果:


y = v.Interface().(float64)fmt.Println(y)
复制代码


对于 fmt.Println 和 fmt.Printf 等都传入的是 empty interface values,在 fmt 内部将其解析出来。因此正确打印 reflect.Value 的内容做法是将 Interface 方法返回值传给 print 函数


fmt.Println(v.Interface())
复制代码


fmt 库后买你有做了更新,它会自动解析 reflect.Value,所以可以这样写,会得到同样的结果:


fmt.Println(v)
复制代码


但是为了代码更加清晰,应保留 Interface() 的调用。总结来说,Interface 方法是 ValueOf 函数的逆向,除了它返回的结果类型是 interface{}

3. 要修改反射对象,它的值必须是可设置的

第三定律是最微妙和令人困惑的,但如果我们从基本原理开始,它很容易理解。


var x float64 = 3.4v := reflect.ValueOf(x)v.SetFloat(7.1) // Error, will cause panic
复制代码


输出结果:


panic: reflect.Value.SetFloat using unaddressable value
复制代码


问题并不在于说 7.1 这个值是不能寻址的,而是 v 是不可设置的。可设置是反射 Value 的一个属性,并不是所有的 Value 都有这个属性。


Value 的 CanSet 能返回 Value 是否可以设置:


var x float64 = 3.4v := reflect.ValueOf(x)fmt.Println("settablility of v:", v.CanSet())
复制代码


输出结果:


settability of v: false
复制代码


在一个不允许设置的 Value 中调用 Set 方法会导致出错,那么什么是可设置的呢?可设置性有点像可寻址性,但更为严格。通过这个属性,反射对象可以修改用于创建反射对象的实际存储。可设置性取决于反射对象是否保存原始项。


var x float64 = 3.4v := reflect.ValueOf(x)
复制代码


在这里将 x 传给 ValueOf,其实是传了一份 x 的拷贝,而不是 x 本身。所以当执行 v.SetFloat(7.1) 的时候如果成功,并不会更新 x 的值,尽管看起来 v 是从 x 创建的,但是它更新的是 x 的拷贝中存储的反射值,x 本身不会受到影响。这将是令人困惑和无用的,所以它是非法的,可设置性是用来避免这个问题的属性。


如果这看起来很奇怪,其实并不奇怪。这其实是一个熟悉的情况下,不寻常的装束。考虑将 x 传递给函数:f(x),如果我们希望函数 f 能够修改 x 的值,但是我们传的是 x 的拷贝,所以如果希望直接修改 x,那么应该把 x 的指针传给 f,也就是f(&x)


这是简单和熟悉的,反射的工作方式也是一样的。如果要通过反射修改 x,必须给反射库一个指针,指向要修改的值。


下面来看一个例子:


var x float64 = 3.4p := reflect.ValueOf(&x)fmt.Println("type of p:", p.Type())fmt.Println("settability of p:", p.CanSet())
复制代码


输出结果:


type of p: *float64settability of p: false
复制代码


反射对象 p 是不可设置的,但它不是我们想设置的 p,它是(实际上)*p。为了得到 p 所指向的对象,我们调用 Value 的 Elem 方法,它直接指向指针,并将结果保存在一个名为 v 的反射值中:


v := p.Elem()fmt.Println("settability of v:", v.CanSet())
复制代码


现在的 v 就是一个可以设置的反射对象了,输出结果为:


settability of v: true
复制代码


这个时候设置来修改 v 的值,那么 x 的值也会发生变化:


v.SetFloat(7.1)fmt.Println(v.Interface())fmt.Println(x)
复制代码


输出结果:


7.17.1
复制代码


反射可能很难理解,但它的功能与语言完全一样,尽管它通过反射类型和值来掩盖所发生的事情。只要记住,反射值需要某个东西的地址,以便修改它们所表示的内容。

结构体 struct

下面一个例子用于分析结构体的 value,创建结构体对象的取址反射对象,因为后面我们想修改它,然后,我们将 typeOfT 设置为它的类型,并使用简单的方法调用遍历字段(有关详细信息,请参阅包反射)。注意,我们从结构类型中提取字段的名称,但字段本身是常规反射。值对象。


type T struct {    A int    B string}t := T{23, "skidoo"}s := reflect.ValueOf(&t).Elem() // 得到结构体的元素typeOfT := s.Type()for i := 0; i < s.NumField(); i++ { // NumField 是结构体的元素的数量    f := s.Field(i)    fmt.Printf("%d: %s %s = %v\n", i,        typeOfT.Field(i).Name, f.Type(), f.Interface())}
复制代码


输出结果:


0: A int = 231: B string = skidoo
复制代码


在这里还有一点关于可设置性,结构体的 T 的 field name 是大写的,可导出的,只有可导出的 field 才可以设置因为 s 包含一个可设置的反射对象,所以我们可以修改结构的字段。


s.Field(0).SetInt(77)s.Field(1).SetString("Sunset Strip")fmt.Println("t is now", t)
复制代码


输出结果:


t is now {77 Sunset Strip}
复制代码


如果我们修改程序,使 s 是从 t 而不是 &t 创建的,那么对 SetInt 和 SetString 的调用就会失败,因为 t 的字段是不可设置的。

总结

三个法则:


  • Reflection goes from interface value to reflection object.

  • Reflection goes from reflection object to interface value.

  • To modify a reflection object, the value must be settable.

参考

The laws of Refelection 这个文章很有趣,有这样一段话,这篇文章写于 2011 年,但是居然在 2022 年,go 1.18 提出泛型的时候加入了这个注释,很棒。



发布于: 刚刚阅读数: 2
用户头像

linlh

关注

还未添加个人签名 2017.12.14 加入

还未添加个人简介

评论

发布
暂无评论
Go反射的三大法则