The Go Blog 01:反射的法则(译文)

反思的法则
罗伯-派克
2011 年 9 月 6 日

引言

计算机中的反射是指程序检查自身结构的能力,尤其是通过类型检查自身结构的能力;它是元编程的一种形式。它也是造成混乱的一个重要原因。

在本文中,我们试图通过解释 Go 中的反射是如何工作的来澄清问题。每种语言的反射模型都不尽相同(许多语言根本不支持反射),但本文是关于 Go 的,因此在本文的其余部分,"反射 "一词应理解为 “Go 语言中的反射”。

2022 年 1 月添加的注释:这篇博文写于 2011 年,早于 Go 中的参数多态性(又称泛型)。尽管由于 Go 语言的发展,文章中没有任何重要的内容变得不正确,但为了避免混淆熟悉现代 Go 语言的人,我们还是对一些地方进行了调整。

类型和接口

因为反射建立在类型系统之上,所以我们先来复习一下 Go 中的类型。

Go 是静态类型的。每个变量都有一个静态类型,即在编译时已知并固定的类型:int、float32、*MyType、[]byte 等等。如果我们声明

type MyInt intvar i int
j MyInt

那么 i 的类型是 int,j 的类型是 MyInt。变量 i 和 j 具有不同的静态类型,尽管它们具有相同的底层类型,但如果不进行转换,就不能相互赋值。

接口类型是类型的一个重要类别,它代表固定的方法集。(在讨论反射时,我们可以忽略在多态代码中使用接口定义作为约束)。接口变量可以存储任何具体(非接口)值,只要该值实现了接口的方法。io.Reader和io.Writer是一对著名的例子,它们是io包中的Reader和Writer类型:

// Reader is the interface that wraps the basic Read method.
type Reader interface {Read(p []byte) (n int, err error)
}// Writer is the interface that wraps the basic Write method.
type Writer interface {Write(p []byte) (n int, err error)
}

任何实现了具有此签名的读(或写)方法的类型都被称为实现了 io.Reader(或 io.Writer)。在本讨论中,这意味着io.Reader 类型的变量可以容纳任何具有读取方法的值:

var r io.Reader
r = os.Stdin
r = bufio.NewReader(r)
r = new(bytes.Buffer)
// and so on

需要明确的是,无论 r 的具体值是什么,r 的类型始终是 io.Reader: Go 是静态类型的,r 的静态类型就是 io.Reader。

接口类型的一个极其重要的例子是空接口:

interface{}

或其对应的别名、

any

它表示方法的空集,任何值都可以满足它,因为每个值都有零个或多个方法。

有人说 Go 的接口是动态类型的,但这是一种误导。它们是静态类型的:接口类型的变量总是具有相同的静态类型,即使在运行时存储在接口变量中的值可能会改变类型,该值也总是满足接口的要求。

我们需要准确地理解这一切,因为反射和接口密切相关。

接口的表示

Russ Cox 写过一篇关于 Go 中接口值表示的详细博文。我们没有必要在此重复全部内容,但有必要做一个简化的总结。

接口类型的变量存储一对值:分配给变量的具体值和该值的类型描述符。更准确地说,值是实现接口的底层具体数据项,而类型描述的是该数据项的完整类型。例如,在

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

从示意代码上看,r 包含 (value, type) 对 (tty、*os.File)。请注意,*os.File 类型实现了 Read 以外的其他方法;尽管接口值只提供了对 Read 方法的访问,但其中的值包含了该值的所有类型信息。这就是我们可以这样做的原因:

w io.Writer
w = r.(io.Writer)

这个赋值中的表达式是一个类型断言;它断言 r 中的项目也实现了 io.Writer,因此我们可以将其赋值给 w。接口的静态类型决定了接口变量可以调用哪些方法,即使里面的具体值可能有更多的方法集。

我们可以继续这样做

var empty interface{}
empty = w

我们的空接口值 empty 将再次包含相同的一对(tty、*os.File)。这很方便:空接口可以容纳任何值,并包含我们所需的关于该值的所有信息。

(这里我们不需要类型断言,因为我们静态地知道 w 满足empty interface{})。在将一个值从 Reader 移到 Writer 的例子中,我们需要明确地使用类型断言,因为 Writer 的方法不是 Reader 方法的子集)。

一个重要的细节是,接口变量内部的变量对总是以 (value, concrete type)的形式存在,而不能以(value, interface type).的形式存在。接口不保存接口值。

现在我们可以进行反射了。

反映的第一定律

1. 反射从接口值到反射对象。

从根本上说,反射只是一种机制,用于检查存储在接口变量中的类型和值对。开始时,我们需要了解包 reflect 中的两种类型: Type and Value。这两种类型可以访问接口变量的内容,而两个简单的函数,即 reflect.TypeOf 和 reflect.ValueOf,可以从接口值中获取 reflect.Type 和 reflect.Value 片段。(此外,从reflect.Value也很容易获取相应的reflect.Type,但我们现在还是把值和类型的概念分开吧)。

让我们从 TypeOf 开始:

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

该程序将打印

type: float64

您可能想知道接口在哪里,因为程序看起来像是将float64变量x传递给reflect.TypeOf,而不是接口值。但它就在那里;正如 godoc 报告的那样,reflect.TypeOf 的签名包含一个空接口:

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

当我们调用 reflect.TypeOf(x) 时,x 首先被存储在一个空接口中,然后将该接口作为参数传递;reflect.TypeOf 会解压该空接口以恢复类型信息。

当然,reflect.ValueOf 函数会恢复值(从这里开始,我们将省略模板,只关注可执行代码):

var x float64 = 3.4
fmt.Println("value:", reflect.ValueOf(x).String())

打印

value: <float64 Value>

(我们明确调用了 String 方法
(我们明确调用 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.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

还有 SetInt 和 SetFloat 等方法,但要使用这些方法,我们需要了解可设置性,即下文讨论的反射第三定律的主题。

反射库有几个特性值得一提。首先,为了保持应用程序接口的简洁,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())   

第二个属性是,反射对象的 Kind 描述的是底层类型,而不是静态类型。如果一个反射对象包含一个用户定义的整数类型的值,如

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

v 的 Kind 仍然是 reflect.Int,尽管 x 的静态类型是 MyInt,而不是 int。换句话说,即使类型可以区分 int 和 MyInt,Kind 也不能。

反射第二定律

2. 反射从反射对象到接口值。

与物理反射一样,Go 中的反射也会产生自己的逆反。

给定一个 reflect.Value,我们可以使用 Interface 方法恢复一个接口值;实际上,该方法将 type and value信息打包回接口表示中,并返回结果:

// Interface returns v's value as an interface{}.
func (v Value) Interface() interface{}

因此,我们可以说

y := v.Interface().(float64) // y 的类型是 float64。
fmt.Println(y)

来打印反射对象 v 所代表的 float64 值。

不过,我们还可以做得更好。fmt.Println、fmt.Printf 等的参数都以空接口值的形式传递,然后由 fmt 包在内部解包,就像我们在前面的示例中所做的那样。因此,要正确打印 reflect.Value 的内容,只需将 Interface 方法的结果传递给格式化打印例程即可:

fmt.Println(v.Interface())

(自本文撰写以来,对 fmt 软件包进行了修改,使其能像这样自动解压缩 reflect.Value,因此我们可以直接说

fmt.Println(v)

就能得到同样的结果,但为了清晰起见,我们在这里保留 .Interface() 调用)。

由于我们的值是 float64,因此我们甚至可以使用浮点格式:

fmt.Printf("value is %7.1e\n", v.Interface())

并在本例中得到

3.4e+00

同样,我们也不需要将 v.Interface() 的结果类型验证为 float64;空接口值内部包含了具体值的类型信息,Printf 将恢复它。

简而言之,Interface 方法就是 ValueOf 函数的逆过程,只不过它的结果总是静态的 interface{} 类型。

重申: 反射从接口值到反射对象,再返回接口值。

反射的第三定律

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

第三定律是最微妙、最容易混淆的,但如果我们从第一条原则出发,还是很容易理解的。

下面是一些不起作用但值得研究的代码。

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

如果运行这段代码,会出现以下提示信息

panic: reflect.Value.SetFloat using unaddressable value

问题不在于值 7.1 不可寻址,而在于 v 不可设置。可设置性是reflection Value的一个属性,并非所有reflection Value都具有该属性。

在我们的例子中,Value 的 CanSet 方法会报告 Value 的可设置性、

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 的副本,因此作为 reflect.ValueOf 参数创建的接口值是 x 的副本,而不是 x 本身。因此,如果语句

v.SetFloat(7.1)

会更新存储在反射值中的 x 的副本,而 x 本身不会受到影响。这样做既混乱又无用,因此是非法的,而可设置性正是用来避免这一问题的属性。

如果这看起来很奇怪,其实不然。实际上,这是一个我们熟悉的情况,只是披上了不寻常的外衣。想想把 x 传递给函数

f(x)

我们不会指望 f 能够修改 x,因为我们传递的是 x 值的副本,而不是 x 本身。如果我们想让 f 直接修改 x,就必须向函数传递 x 的地址(即指向 x 的指针):

f(&x)

这既简单又熟悉,反射也是如此。如果我们想通过反射修改 x,就必须给反射库一个指向我们要修改的值的指针。

让我们开始吧。首先,我们像往常一样初始化 x,然后创建一个指向它的反射值,称为 p。

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。为了获取 p 指向的内容,我们调用了 Value 的 Elem 方法,该方法通过指针进行间接操作,并将结果保存在名为 v 的reflection Value 中:

v := p.Elem()
fmt.Println("settability of v:", v.CanSet())

正如输出结果所示,现在 v 是一个可设置的反射对象、

settability of v: true

由于 v 代表 x,我们终于可以使用 v.SetFloat 来修改 x 的值了:

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

正如预期的那样,输出结果是

7.1
7.1

反射可能很难理解,但它确实在做语言所做的事情,尽管通过反射类型和值可以掩盖正在发生的事情。请记住,反射值需要某些东西的地址,以便修改它们所代表的内容。

Structs

在我们前面的示例中,v 本身并不是一个指针,它只是从指针派生出来的。出现这种情况的常见方法是使用反射来修改结构体的字段。只要我们有结构体的地址,就可以修改它的字段。

下面是一个分析 struct value t 的简单示例。我们用结构体的地址创建反射对象,因为我们稍后要修改它。然后,我们将 typeOfT 设置为其类型,并使用直接的方法调用遍历字段(详见包 reflect)。请注意,我们从结构类型中提取了字段的名称,但字段本身是普通的 reflect.Value 对象。

type T struct {A intB 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

这里还顺便介绍了一个关于可设置性的要点:T 的字段名是大写的(导出),因为只有结构体的导出字段才是可设置的。

因为 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 的字段将不可设置。

Conclusion

这里又是反射法则:
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.

一旦你理解了这些定律,Go中的反射就会变得更容易使用,尽管它仍然很微妙。这是一个强大的工具,除非必要,否则应该小心使用。
还有很多我们没有涉及到的问题——在channel上发送和接收、分配内存、使用slices和map、调用方法和函数——但是这篇文章已经足够长了。我们将在后面的文章中讨论其中的一些主题。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/45513.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

计算机网络----CRC冗余码的运算

目录 1. 冗余码的介绍及原理2. CRC检验编码的例子3. 小练习 1. 冗余码的介绍及原理 冗余码是用于在数据链路层的通信链路和传输数据过程中可能会出错的一种检错编码方法&#xff08;检错码&#xff09;。原理&#xff1a;发送发把数据划分为组&#xff0c;设每组K个比特&#…

详解strcmp函数

strcmp函数是用来比较两个字符串的&#xff0c;按理来说&#xff0c;比较结果只有两种&#xff1a;相同或不同。但是&#xff0c;事实上&#xff0c;strcmp函数在设计时会有三种情况&#xff0c;下面详细介绍&#xff1a; 这个函数的输入为两个字符串的首元素地址&#xff08;即…

MySQL 8.0.31 登录提示caching_sha2_password问题解决方法

MySQL 8.0.31 登录提示caching_sha2_password问题解决方法 MySQL 8.0.31 使用了 caching_sha2_password 作为默认的身份验证插件&#xff0c;这可能导致一些旧的客户端和库无法连接到服务器。以下是一些解决此类问题的常见步骤和建议&#xff1a; 确保MySQL服务正在运行&#…

C++11并发与多线程笔记(13) 补充知识、线程池浅谈、数量谈、总结

C11并发与多线程笔记&#xff08;13&#xff09; 补充知识、线程池浅谈、数量谈、总结 1、补充一些知识点1.1 虚假唤醒&#xff1a;1.2 atomic 2、浅谈线程池&#xff1a;3、线程创建数量谈&#xff1a; 1、补充一些知识点 1.1 虚假唤醒&#xff1a; notify_one或者notify_al…

Excel/PowerPoint折线图从Y轴开始(两侧不留空隙)

默认Excel/PowerPoint折线图是这个样子的&#xff1a; 左右两侧都留了大块空白&#xff0c;很难看 解决方案 点击横坐标&#xff0c;双击&#xff0c;然后按下图顺序点击 效果

Docker运行Nacos容器,过一会就报错`UnsatisfiedDependencyException`

Docker运行Nacos容器&#xff0c;过一会就报错UnsatisfiedDependencyException 问题背景&#xff1a; 最近要上线一个项目&#xff0c;由于要使用Nacos作为服务注册中心&#xff0c;为了方便&#xff0c;我就打算直接使用Docker部署Nacos&#xff0c;没想到Nacos启动没一会就嗝…

STM32 F103C8T6学习笔记10:OLED显示屏GIF动图取模—简易时钟—动图手表的制作~

今日尝试做一款有动图的OLED实时时钟&#xff0c;本文需要现学一个OLED的GIF动图取模 其余需要的知识点有不会的可以去我 STM32 F103C8T6学习笔记 系列专栏自己查阅把&#xff0c;闲话不多&#xff0c;直接开肝~~~ 文章提供源码&#xff0c;测试工程下载&#xff0c;测试效…

(二)结构型模式:5、装饰器模式(Decorator Pattern)(C++实例)

目录 1、装饰器模式&#xff08;Decorator Pattern&#xff09;含义 2、装饰器模式的UML图学习 3、装饰器模式的应用场景 4、装饰器模式的优缺点 5、C实现装饰器模式的简单实例 1、装饰器模式&#xff08;Decorator Pattern&#xff09;含义 装饰模式&#xff08;Decorato…

负载均衡下的 WebShell 连接

目录 负载均衡简介负载均衡的分类网络通信分类 负载均衡下的 WebShell 连接场景描述难点介绍解决方法**Plan A** **关掉其中一台机器**&#xff08;作死&#xff09;**Plan B** **执行前先判断要不要执行****Plan C** 在Web 层做一次 HTTP 流量转发 &#xff08;重点&#xff0…

HummingBird 基于 Go 开源超轻量级 IoT 物联网平台

蜂鸟&#xff08;HummingBird&#xff09; 是 Go 语言实现的超轻量级物联网开发平台&#xff0c;包含设备接入、产品管理、物模型、告警中心、规则引擎等丰富功能模块。系统采用GoLang编写&#xff0c;占用内存极低&#xff0c; 单物理机可实现百设备的连接。 在数据存储上&…

MATLAB | 七夕节用MATLAB画个玫瑰花束叭

Hey又是一年七夕节要到了&#xff0c;每年一次直男审美MATLAB绘图大赛开始hiahiahia&#xff0c;真的这些代码越写越不知道咋写&#xff0c;又不想每年把之前的代码翻出来再发一遍&#xff0c;于是今年又对我之前写的老代码进行了点优化组合&#xff0c;整了个花球变花束&#…

人工智能大模型加速数据库存储模型发展 行列混合存储下的破局

数据存储模型 ​专栏内容&#xff1a; postgresql内核源码分析手写数据库toadb并发编程toadb开源库 个人主页&#xff1a;我的主页 座右铭&#xff1a;天行健&#xff0c;君子以自强不息&#xff1b;地势坤&#xff0c;君子以厚德载物. 概述 在数据库的发展过程中&#xff0c;关…

MySQL5.7数据目录结构

以CentOS7为例&#xff0c;数据目录为/var/lib/mysql/&#xff0c;其内容如下&#xff1a; [rootscentos szc]# ll /var/lib/mysql/ total 122952 -rw-r----- 1 mysql mysql 56 Jan 15 16:02 auto.cnf -rw------- 1 mysql mysql 1680 Jan 15 16:02 ca-key.pem -rw-r…

系统架构设计专业技能 · 软件工程之需求工程

系列文章目录 系统架构设计高级技能 软件架构概念、架构风格、ABSD、架构复用、DSSA&#xff08;一&#xff09;【系统架构设计师】 系统架构设计高级技能 系统质量属性与架构评估&#xff08;二&#xff09;【系统架构设计师】 系统架构设计高级技能 软件可靠性分析与设计…

深入浅出Pytorch函数——torch.nn.Module.apply

分类目录&#xff1a;《深入浅出Pytorch函数》总目录 相关文章&#xff1a; 深入浅出Pytorch函数——torch.nn.Module 递归地将函数fn应用于每个子模块及self&#xff0c;子模块由.children()返回。典型的用法包括初始化模型的参数&#xff08;可以参考torc.nn.init&#xff0…

Qt快速学习(一)--对象,信号和槽

目录 1.Qt概述 1.1 什么是Qt 2.2 手动创建 2.3 pro文件 2.4 一个最简单的Qt应用程序 3 第一个Qt小程序 3.1 按钮的创建 3.2 对象模型&#xff08;对象树&#xff09; 3.3 Qt窗口坐标体系 4 信号和槽机制 4.1 系统自带的信号和槽 4.2 自定义信号和槽 4.3信号槽的拓展 4…

Edge浏览器免费使用GPT3.5

搜索sider,安装Sidebar插件 注册账号即可每天免费使用30次。 Sider: ChatGPT侧边栏,GPT-4, 联网, 绘图

C++ 的关键字(保留字)完整介绍

1. asm asm (指令字符串)&#xff1a;允许在 C 程序中嵌入汇编代码。 2. auto auto&#xff08;自动&#xff0c;automatic&#xff09;是存储类型标识符&#xff0c;表明变量"自动"具有本地范围&#xff0c;块范围的变量声明&#xff08;如for循环体内的变量声明…

机器学习深度学习——NLP实战(情感分析模型——RNN实现)

&#x1f468;‍&#x1f393;作者简介&#xff1a;一位即将上大四&#xff0c;正专攻机器学习的保研er &#x1f30c;上期文章&#xff1a;机器学习&&深度学习——NLP实战&#xff08;情感分析模型——数据集&#xff09; &#x1f4da;订阅专栏&#xff1a;机器学习&…

非科班如何丝滑转码

文章目录 1. 引言2. 如何规划才能实现转码2.1 确定目标和动机2.2 学习路径规划2.3 寻找学习资源2.4 制定学习计划 3. 计算机岗位发展前景3.1 编程岗位3.2 数据分析和人工智能岗位3.3 网络和系统管理岗位 4. 现阶段转码的建议4.1 学习编程基础4.2 选择合适的编程语言4.3 实践和项…