反射的三大laws,laws博主实在找不到什么好的翻译,定律?法律?规则?好像都不适用。就直接使用law吧。本篇是读go官方博客后,加入了自己的部分梳理。

0.前言

在讲清楚go语言中的反射是如何工作之前,需要先回顾一下go语言的类型,因为反射是建立在类型系统之上的,所以复习类型系统是很有必要的,让我们温故而知新。

1.静态类型

在go语言中,每个变量都有一个静态类型,这句话等价于,在编译时,只有一种类型是已知的,且固定。

type MyInt int //类型定义
var i int //int类型
var j MyInt //MyInt类型

尽管上面i与j有相同的底层类型,但是i, j是不同的静态类型,就不能相互赋值,除非转换。

2.接口类型

go语言中有一个重要的类型,就是接口类型。接口代表固定的方法集。接口变量可以存储任何具体的(非接口的)值,只要该值实现了接口的方法,这就是 duck-type programming。重要的是要清楚,无论接口变量的具体值(实现)是什么,接口变量的类型总是接口:因为Go是静态类型的

3.空接口

代表方法集为空。任何值都满足空接口,因为任何值都有0-n个方法。有些人说,go的接口就是动态类型,这是误导。他们还是静态类型。**接口类型的变量总是具有相同的静态类型,**即使在运行的过程中,存储在接口变量中的值可能改变类型,但是值也总是满足接口。

以上知识,我们需要精确的了解,在脑海中形成惯性思维,因为反射和接口是密切相关的。

4.接口表示

接口类型的变量存储一对,着重理解这个,后面我依然会提到对:

  • 赋给该接口变量的具体值
  • 以及该值的类型描述符

更准确地说,这个是实现接口的底层具体数据项,而类型描述底层数据项的完整类型

var r io.Reader
tty, err := os.OpenFile("/dev/tty", os.O_RDWR, 0)
if err != nil {
  return nil, err
}
r = tty // 赋值

r=tty, *os.File, 当然os.File还实现了io.Reader以外的方法。**即使接口的值只提供对Read方法的访问,内部的值携带关于该值的所有类型信息。**所以我们能过做一下操作:

var r io.Reader
tty, err := os.OpenFile("/dev/tty", os.O_RDWR, 0)
if err != nil {
    return nil, err
}
r = tty

var w io.Writer 
w = r.(io.Writer) // 类型断言

w=r.(io.Writer) 赋值表达式是一个类型断言,它断言r中的项也实现了 io.Writer .

赋值之后,wr一样,拥有的一对数据(tty, *os.File)

接口的静态类型决定了可以用接口变量调用哪些方法,即使里面的具体值可能有一组更大的方法也调不了。除非断言为其他接口。

继续看:

var r io.Reader
tty, err := os.OpenFile("/dev/tty", os.O_RDWR, 0)
if err != nil {
    return nil, err
}
r = tty

var w io.Writer  
w = r.(io.Writer) 
var empty interface{}  //空接口
empty = w

空接口值 empty 再一次包含同样的 (tty, *os.File)。这就很方便了,方便在哪里?这就出现我们学习go语言时,各种’老师’告诉我们的结论:空接口可以保存任何值,并包含我们可能需要的关于该值的所有信息。完美。

这里我们不需要类型断言,因为静态地知道w满足空接口。在将一个值从Reader移到Writer的例子中,我们需要显式地使用类型断言,因为Writer的方法不是Reader的方法的子集。

  • 换言之,如果某个方法集是另外一个方法集的子集,就不需要类型断言,空接口代表的方法集就是任何接口代表的方法集的子集

另外一个重要的细节是,接口内的对总是具有形式(值,具体类型),而不能是具有形式(值,接口类型)。接口不保存接口值

5.反射的law

5.1 从接口值到反射对象

反射只是一种检查存储在接口变量中的类型与值对的机制。所以,一开始这里有两个reflect 包的概念需要了解:

  • Type
  • Value

这两种类型允许访问接口变量的内容:

reflect.TypeOf()  // reflect.Type
reflect.ValueOf() // reflect.Value 

看如下代码:

package main

import (
    "fmt"
    "reflect"
)

func main() {
    var x float64 = 3.4
    fmt.Println("type:", reflect.TypeOf(x)) //type: float64
}

这里的接口在哪里?因为程序看起来像是在传递float64变量x,而不是接口值。不妨跟踪到源码:

// TypeOf returns the reflection Type of the value in the interface{}.
func TypeOf(i interface{}) Type

当我们调用 reflect.TypeOf(x) x首先被存储在空接口作为参数传递reflect.TypeOf 解压空接口恢复类型信息

  • 存储空接口
  • 解压空接口
var x float64 = 3.4
fmt.Println("value:", reflect.ValueOf(x).String()) //value: <float64 Value>

显式地调用String方法,因为默认情况下fmt包挖掘reflect. Value以显示其中的具体值。String方法不这样做。

reflect.Typereflect.Value 拥有很多方法让我们检查和操作它们。

  • Value有一个Type方法返回reflect.ValueType
  • TypeValue有一个Kind方法用以返回一个常量,指示存储的项的类型
var x float64 = 3.4
v := reflect.ValueOf(x)
fmt.Println("type:", v.Type())
fmt.Println("kind is float64:", v.Kind() == reflect.Float64)
fmt.Println("value:", v.Float())
type: float64
kind is float64: true
value: 3.4

反射库有几个值得注意的属性。首先,为了保持API的简单性,Value的“getter”和“setter”方法操作可以保存值的最大类型,这是什么意思?例如:所有有符号整数的int64。也就是说,Value的Int方法返回一个int64值,SetInt值接受一个int64值; 可能需要将其转换为所涉及的实际类型:

var x uint8 = 'x'
v := reflect.ValueOf(x)
fmt.Println("type:", v.Type())                            // uint8.
fmt.Println("kind is uint8: ", v.Kind() == reflect.Uint8) // true.
x = uint8(v.Uint())                                       // v.Uint returns a uint64.

第二个属性是反射对象的Kind描述基础类型,而不是静态类型。也就是说,kind是读不了定义类型的:

type MyInt int
var x MyInt = 7
v := reflect.ValueOf(x)

v的kind仍然是 reflect.Int ,尽管x的静态类型是 MyInt . 换句话说,Kind不能区分int和MyInt=Kind不能区分静态类型

5.2 从反射对象到接口值

就像物理现象中的反射一样,go的反射可以生成相反面。

给定一个 reflect.Value 就能恢复接口值。使用 Interface 方法。实际上,该方法将类型和值信息打包回接口表示中,并返回结果:

// Interface returns v's value as an interface{}.
func (v Value) Interface() interface{}
y := v.Interface().(float64) // y will have type float64.
fmt.Println(y)

注意在fmt.Println, fmt.Printf等方法中,是不需要类型断言的,因为这些方法是以空接口传参,然后在fmt内部解压。空接口值已经拥有具体值以及类型信息,然后在方法内部会恢复它。因此:

fmt.Println(v.Interface())

而言之,’ Interface ‘方法是’ ValueOf ‘函数的逆函数,只是它的结果总是静态类型’ Interface {} ’ .

5.3 如果要修改反射对象,该值必须是可设置的

看标题,这条law是最令人困惑的,但如果我们从最基本的原理开始,这很容易理解。

var x float64 = 3.4
v := reflect.ValueOf(x)
v.SetFloat(7.1) // Error: will panic.
panic: reflect.Value.SetFloat using unaddressable value

这里panic的原因不是7.1不可寻址。而是v不可设置。可设置性(settability)是反射Value的一个属性,但是却不是所有的Value都拥有。 ValueCanSet方法可以知道可设置性:

var x float64 = 3.4
v := reflect.ValueOf(x)
fmt.Println("settability of v:", v.CanSet())
settability of v: false

不能设置,而设置,肯定要报错,那么问题来了。什么是可设置性???

可设置性有点像可寻址,但是更严格。它是反射对象可以被修改并创建反射对象实际存储的属性。可设置性是由反射对象的原始项。

来继续看上面的代码:

var x float64 = 3.4
v := reflect.ValueOf(x)

我们向reflect.ValueOf传了一个x变量的副本,此时内部接口值是x的副本,而不是x本身。如果v.SetFloat(7.1)被允许且成功,它不会更新x(这个可以理解,这是副本),相反,它会更新存储在反射值中的’ x ‘副本,而’ x ‘本身不会受到影响。这是混乱的,也没什么用的,所以它是非法的,可设置性是用来避免这个问题的属性。

看起来有些奇怪,其实细想一下也不奇怪,就是一个新瓶装旧酒,套用赵本山的小品,换了个马甲就不认识,别笑,我觉得这是很正常思维现象,比如一个函数f(x),我们不会期望f函数可以修改x,因为这是x的副本,而不是x本身,如果f函数想直接改变x,那么传递至函数的应该是x的地址,也就是指针:f(&x),反射也是如此。如果我们想通过反射来修改’ x ‘,必须给反射库一个指向我们想要修改的值的指针。

var x float64 = 3.4
p := reflect.ValueOf(&x) // Note: take the address of x.
fmt.Println("type of p:", p.Type())
fmt.Println("settability of p:", p.CanSet())
type of p: *float64
settability of p: false

反射对象p不能设置,但是p也不是我们想设置的,我们想更改的是*p,调用ValueElem 方法获取p的指向,这间接通过指针.

var x float64 = 3.4
p := reflect.ValueOf(&x) // Note: take the address of x.
v := p.Elem() // save the result in a reflection `Value`
fmt.Println("settability of v:", v.CanSet())

v.SetFloat(7.1)
fmt.Println(v.Interface())
fmt.Println(x)

这样v就是一个可设置的反射对象

settability of v: true
7.1
7.1

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

6.反射之结构体

结构体又有有一些特别的地方。

  • 我们从结构类型中提取字段的名称,但字段本身是常规reflect. Value对象。
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++ {
    f := s.Field(i)
    fmt.Printf("%d: %s %s = %v\n", i,
        typeOfT.Field(i).Name, f.Type(), f.Interface())
}
0: A int = 23
1: B string = skidoo
  • 关于 可设置性 需要补充一点,就是结构体的字段名称是大写字母开头(导出),因为只有导出的结构体字段才是settable
s.Field(0).SetInt(77)
s.Field(1).SetString("Sunset Strip")
fmt.Println("t is now", t)

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

7.结论

  • 每个变量都有一个静态类型,也就是说,变量的类型是在编译时是已知固定的。
  • 要区分底层类型与静态类型
  • 无论接口变量的具体值(实现)是什么,接口变量的类型总是接口,因为go是静态类型,所以接口类型的变量总是具有相同的静态类型。
  • 接口类型的变量:具体值+这个值的类型描述
  • 反射三板斧:
    • 从接口值到反射对象(解压空接口以获得接口值的信息)
    • 从反射对象到接口值(从解压空接口获得的接口值信息再恢复为接口值)
    • 修改反射对象的值,则该值必须为可设置的(settable)

参考链接

https://go.dev/blog/laws-of-reflection