惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
爱范儿
爱范儿
H
Help Net Security
V
Visual Studio Blog
J
Java Code Geeks
Stack Overflow Blog
Stack Overflow Blog
Microsoft Security Blog
Microsoft Security Blog
Apple Machine Learning Research
Apple Machine Learning Research
MyScale Blog
MyScale Blog
The Cloudflare Blog
Martin Fowler
Martin Fowler
D
Docker
腾讯CDC
F
Fortinet All Blogs
雷峰网
雷峰网
GbyAI
GbyAI
G
Google Developers Blog
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
Recent Announcements
Recent Announcements
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
Blog — PlanetScale
Blog — PlanetScale
Engineering at Meta
Engineering at Meta
博客园 - 聂微东
博客园 - 叶小钗

Yesterday17's Blog

2026 新年解密红包 / Melody Flag | Yesterday17's Blog 谈谈 Iori 的设计思路(二):如何实现一个 Showroom 录制工具? | Yesterday17's Blog 谈谈 Iori 的设计思路(一):从 Nico Timeshift 说起 | Yesterday17's Blog Iori Minyami 0.1.0 发布 | Yesterday17's Blog 2025 新年解密红包 / Melody Flag | Yesterday17's Blog 使用 Cloudflare Warp 解决罗森票务的海外登录问题 | Yesterday17's Blog How To Blog 04: The Astro v5 Era | Yesterday17's Blog 谈谈 tokio::select! 的公平性 | Yesterday17's Blog Learning Pingora 05 - Connect with TLS | Yesterday17's Blog Leaving Bytedance | Yesterday17's Blog 大橋彩香 AsiaTour「Reflection」上海公演 个人向记录 & Repo | Yesterday17's Blog Recoving from burnout - What happened? | Yesterday17 Yubikey 重建手册 | Yesterday17's Blog How To Blog 03: Heimus | Yesterday17's Blog 🪧 Blog Migration Accouncement | Yesterday17's Blog Learn Your IDE - VSCode 是如何仅重启插件的? | Yesterday17's Blog How To Blog 02: Astro❤️Password | Yesterday17's Blog How To Blog 01: Why, How, and the Future | Yesterday17's Blog Learning Pingora 04 - Establish L4 Connection | Yesterday17's Blog Learning Pingora 03 - Upstreams and Peers | Yesterday17's Blog Learning Pingora 02 - A Simple HTTP Server | Yesterday17's Blog Learning Pingora 01 - Getting Started | Yesterday17's Blog 2024 新年解密红包 / Melody Flag | Yesterday17's Blog 向新的一年飞驰——记录 2023 | Yesterday17's Blog 「サクラノ刻」对话选摘(2) | Yesterday17's Blog PGP Key Revocation 注销声明 | Yesterday17's Blog 「サクラノ刻」对话选摘(1) | Yesterday17's Blog 2023 新年解密红包 / Melody Flag | Yesterday17's Blog 『蒼の彼方のフォーリズム』通关感想 | Yesterday17's Blog 单显卡直通教程 | Yesterday17's Blog
Go 学习笔记 01 - 找准反射之道 | Yesterday17's Blog
Yesterday17 · 2020-02-18 · via Yesterday17's Blog

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

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

if reflect.ValueOf(addr).IsNil() {

// operation

}

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

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

简介

无论你听说过反射或否,在这里我们简单的给出一个定义:反射是在运行时获取/修改对象的一种手段。我们知道我默认你们知道,通过 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