Skip to content

Go 学习笔记 01 - 找准反射之道

Published: at 02:52

最近准备好好的补一下 Go 的基础内容,正好反射这一点也是我比较关注的。

之前在 clash 那里发的 issue 就遇到了这样一个 golang 经典陷阱:Typed nilnil 并不相等,使用 xxx != nil 会得到永真的结果。这个问题的暂时解决方案就和反射有关:

1
if reflect.ValueOf(addr).IsNil() {
2
// operation
3
}

当然了,这样的解决方案是暂时的,我们不能在每个这样的场合都通过反射来解决问题,但这勾起了我对 Go 反射的兴趣。而想要了解反射,reflect给了我们一个不错的建议

读完全文,深有感触。正所谓想要学好反射,不仅在于找准反射之道,关键在于找准反射之道。那我们就开始吧(

简介

无论你听说过反射或否,在这里我们简单的给出一个定义:反射是在运行时获取/修改对象的一种手段。我们知道我默认你们知道,通过 Java 的反射,我们可以获取、修改 private,甚至是 final 的内容,这一点对于 golang 来说也不例外。只是与 Javaclass 机制不同,golang 的类型是零散的,因此无法做到像 Java 那么“动态”。

类型(Type)

Go 的类型,相信大家都很熟悉了。在平时使用的过程中,我们已经习惯了这样的使用:

1
type AAA struct {
2
// struct body
3
}
4
5
type B AAA // here B is alias to AAA
6
// Think the comment above, is it right?

正如最后一行注释所问,B 真的是 AAA 的别名吗?答案是:是、但也不是。这里我们先解释“不是”的原因,而“”的部分则留给下文来细细阐述。

我们知道,如果定义了一个类型的“别名”,那这时候,想要把它直接赋值给原类型是无法隐式做到的,你必须显式地完成这一转换过程。那是因为 Go 是一门静态类型的语言,而别名前后的类型是不一样的。从这个角度来讲,B 并不是 AAA 的别名,它只是和 A 结构相同的兄弟(きょうだい)而已。

前置知识

接口(interface)

在介绍 reflect 之前,我们先提出一个问题:你们认识中的 interface 是什么样的?

不少人可能会带有 Java 遗留下来的固有印象:interface 是那个 implements 的东西,在 golang 里不需要 implements 了,所以更加灵活。

这样的理解,怎么说呢,之前我也是这么认为的,各个语言的 interface 总该差不多吧。但在读完这篇文章之后,我发现我错的离谱。

Go 中的 interface 是什么呢?

实质上,它只是一个方法的集合。在接口中定义了一定数量的方法(数量也可以为0),而实现了这些方法的类型称作实现了这个接口。

有一种说法认为,GoJava 接口的区别在于 Java 需要显式地声明实现,而 Go 是隐式的。我认为这种说法是表现上的事实事实上的误导。我们不妨做一个实验:

1
package main
2
3
import "fmt"
4
5
type InterfaceA interface {
6
Write(data string) error
7
}
8
type InterfaceB interface {
9
Read() string
10
Write(data string) error
11
}
12
13
type Implement string
14
func (i *Implement) Read() string {
15
return string(*i)
16
}
17
func (i *Implement) Write(data string) error {
18
*i = Implement(data)
19
return nil
20
}
21
22
func main() {
23
var impl Implement = "test"
24
var ia InterfaceA = &impl
25
_ = ia.Write("After InterfaceA.")
26
var ib InterfaceB = &impl
27
fmt.Println(ib.Read())
28
_ = ib.Write("After InterfaceB.")
29
fmt.Println(impl)
30
}

在这个例子中,我们定义了两个 interface 和一个 struct。大家可能发现了,B 其实是包含了 A 的,这个例子的输出也一点都不出乎意料。那通过这个例子可以表现出什么呢?

它揭示出了这样的一个事实:Go 的接口实质是方法的集合,只要方法定义相同,那它们就是相同的

在这个基础上,我们再回来看这样一个特殊的类型:interface{}。是不是有点感觉了?这其实就是一个没有任何方法的接口,从方法的集合角度来看,这就是一个方法的空集。正因为空集是所有集合的子集,因此任何接口类型都可以转换为 interface{} 类型。

这样的结构也决定了 Go 的接口是真正意义上的“接口”:只要两个口子使用的标准一样、尺寸一样、参数一样,那它就是一样的,而无关生产厂家(声明在何处,以何名称声明);所有生产出来的产品(Type),只要它符合这个标准,那它就可以说是这个标准下的“同类产品”。

说完了这件最重要的事情,最后补充一点大家都知道的吧。接口是静态类型的存在;接口内存储的是指针类型;接口存储的类型是其定义类型或者 nil

反射

终于,我们到了本文的主体部分,但某种意义上也是最为简单的部分(当然前提是上文的内容你都理解了)。

如果按照那篇博客的方式来讲的话,它将反射之道归结成了三条规则;而在本文,我们希望摆脱这样的束缚,因为这三条规则并不平衡。我们把它们罗列出来:

从这三条来看,其实前两条更像是“设计准则”一样的存在,和第三条相比,其重要性完全不同。因此,这里我们不完全遵循原文的讲解思路。我会用我理解的重要程度对反射作出描述。

接口的值(Value)

不知道大家还记不记得上文留下来的这个问题:

B 真的是 AAA 的别名吗?

上文中,我们回答了“不是”的原因,这里我们来回答“是”的理由。观察如下代码:

1
package main
2
import (
3
"fmt"
4
"reflect"
5
)
6
7
type float114514 float64
8
9
func main() {
10
var f1 float114514 = 1919.810
11
v := reflect.ValueOf(f1)
12
fmt.Println(v.Kind())
13
}

这段代码的输出是什么呢?

float64

这就涉及到了反射中的一个概念:Kind,以及我自认为这门语言最浪漫的地方。

如果你去看 Type 的定义,你会看到这样的一段内容:

1
// A Kind represents the specific kind of type that a Type represents.
2
// The zero Kind is not a valid kind.
3
type Kind uint
4
const (
5
Invalid Kind = iota
6
Bool
7
Int
8
Int8
9
Int16
10
Int32
11
Int64
12
Uint
13
Uint8
14
Uint16
15
Uint32
16
Uint64
17
Uintptr
18
Float32
19
Float64
20
Complex64
21
Complex128
22
Array
23
Chan
24
Func
25
Interface
26
Map
27
Ptr
28
Slice
29
String
30
Struct
31
UnsafePointer
32
)

第一行的注释清晰地告诉了我们一个试试:下面罗列的所有 Kind,就是这门语言中所有的真实类型。不论你具体如何书写,到最后都可以表现为这些元素的相互运算。这也是反射之所以能够存在的原因,正因为这个世界是可知的,我们才可以以这样仿佛“动态”的方式去完成我们想做的任务。

所以这时候如果你问我它是别名吗?我想,答案就如同上文所讲的那般不确定了。是也不是,这是对这个问题最好的回答。

可以改变的极限(CanSet)

这条对应的就是那三条中的第三条,因为这条确实重要,因此我也遵循原文的思路讲起。

我们知道,反射的一大作用就是动态修改。但有些东西是不能修改的。拿 C 来举例子吧,C 中的字符串字面量就是无法修改的,其在编译时就确定了。Go 虽然和其不大一样,但也存在反射无法修改的东西。

回顾我们学习的编程语言,赋值是基础中的基础。在赋值时,任何一本教材都会这样教给我们:[Variable Name] = <ToAssign>,而反之则不一定成立。这是因为 <ToAssign> 可能为数字,而单纯的修改数字“字面量”是非法的。

到了 Go 中,又多了一种情况:试图修改非指针的类型。这里我们拿原文的例子:

1
var x float64 = 3.4
2
v := reflect.ValueOf(x)
3
v.SetFloat(7.1) // Error: will panic.

这里会抛出 panic 的原因很简单:x 不是指针,reflect.ValueOf 获得的其实和直接在 x 处写 3.4 的效果是一样的。如果要判断某种情况下是否可写,可以通过 v.CanSet() 完成。

那什么内容是可写的呢?内存中的某一段是可写的。因此这就需要我们在传入 ValueOf 时传入一个指针,然后修改其指向的内容。在 Go 中,通常是这样完成的:

1
var x float64 = 3.4
2
p := reflect.ValueOf(&x) // Note: take the address of x.
3
v := p.Elem()
4
v.SetFloat(7.1)

最后摘一句原博中的话吧:

Reflection can be hard to understand but it’s doing exactly what the language does, albeit through reflection Types and Values that can disguise what’s going on.

但对我而言,我觉得这句话可以改成:

Reflection is romantic because it’s doing exactly what the language does. Through reflect Types and Values you can know exactly what’s going on.

それではww