Go 学习笔记(1): 找准反射之道

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

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

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

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

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


それでは今夜も初めていきましょう。

水瀬いのり MELODY FLAG、今夜もわたしとゆう旗をあなたの心に立てていきますwww


简介

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

类型(Type)

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

type AAA struct {
  // struct body
}

type B AAA // here B is alias to AAA
// 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 是隐式的。我认为这种说法是表现上的事实事实上的误导。我们不妨做一个实验:

package main

import "fmt"

type InterfaceA interface {
	Write(data string) error
}
type InterfaceB interface {
	Read() string
	Write(data string) error
}

type Implement string
func (i *Implement) Read() string {
	return string(*i)
}
func (i *Implement) Write(data string) error {
	*i = Implement(data)
	return nil
}

func main() {
	var impl Implement = "test"
	var ia InterfaceA = &impl
	_ = ia.Write("After InterfaceA.")
	var ib InterfaceB = &impl
	fmt.Println(ib.Read())
	_ = ib.Write("After InterfaceB.")
	fmt.Println(impl)
}

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

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

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

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

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

反射

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

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

  • 反射使我们能够通过接口到反射对象
  • 反射使我们能够从反射对象返回到接口
  • 要修改的反射对象必须可修改的。

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

接口的值(Value)

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

B 真的是 AAA 的别名吗?

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

package main
import (
	"fmt"
	"reflect"
)

type float114514 float64

func main() {
	var f1 float114514 = 1919.810
	v := reflect.ValueOf(f1)
	fmt.Println(v.Kind())
}

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

float64

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

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

// 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
)

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

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

可以改变的极限(CanSet)

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

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

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

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

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

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

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

var x float64 = 3.4
p := reflect.ValueOf(&x) // Note: take the address of x.
v := p.Elem()
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

暂无评论

发送评论 编辑评论


				
上一篇
下一篇