1. rtype 与 abi.Type:同一块内存的两种视图
Go 1.22 前后,运行时类型元数据从 runtime._type 迁入了 internal/abi.Type;到 1.23/1.24,reflect 的 rtype 进一步改成了单字段包装结构体 (不再是别名,为的是给字段访问加 noescape 收口,见 value.go 里 Value.typ() 的 abi.NoEscape):
swift
// reflect/type.go:319
type rtype struct {
t abi.Type
}
// reflect/type.go:724 ------ 两者仍是同一块内存的两种视图,只是指针 reinterpret,零成本
func toRType(t *abi.Type) *rtype {
return (*rtype)(unsafe.Pointer(t))
}
internal/abi/type.go 中的定义(这是每一份类型元数据的"公共头部"):
go
type Type struct {
Size_ uintptr
PtrBytes uintptr // 类型中(前缀)可包含指针的字节数
Hash uint32 // 类型哈希,map/类型 switch 快速比较用
TFlag TFlag // 额外标志位
Align_ uint8 // 作为变量时的对齐
FieldAlign_ uint8 // 作为结构体字段时的对齐
Kind_ Kind // 种类枚举(27 种)
Equal func(unsafe.Pointer, unsafe.Pointer) bool // 相等比较函数
GCData *byte // GC 指针位图
Str NameOff // 类型名字符串(相对模块元数据的偏移)
PtrToThis TypeOff // 指向"*当前类型"的元数据偏移,可为 0
}
Hash:类型的"指纹"
编译期就算好的 32 位指纹,刻在类型身份证上,运行时不再重算。用途是给各种缓存表当索引键,不是比较两个值:
- 最大用户:接口断言/赋值缓存(itab 表) 。
i.(Reader)或把值赋给接口变量时,runtime 要查"这个具体类型实现这个接口吗、方法表在哪",查过一次就缓存进全局哈希表。定位槽位的键 =接口类型的Hash ^ 具体类型的Hash(runtime/iface.go:30),命中槽位后再比类型指针确认------"先比哈希再比指针"的完整出处在这里。 - reflect 动态拼类型(
StructOf等)的缓存也拿字段类型的 Hash 拼缓存键(reflect/type.go:2099)。 - 纠正:
map[any]V查 key 的哈希是按 key 数据算的 (typehash,runtime/alg.go:201),不用这个字段。map 的流程是"哈希定位桶 → 桶内先比类型指针 → 再调 Equal"。
Equal:每种类型自带的"裁判"
"两个值相等"的规则手册每种类型都不同:int 比 8 字节;string 先比长度再比内容;struct 逐字段请各自的裁判;float 要遵守 NaN != NaN(所以不能无脑 memcmp)。于是在类型元数据里挂一个函数指针 ,== 的活儿外包给它。
接口比较 a == b 的完整流程(efaceeq,runtime/alg.go:368):
ini
类型指针不同 ──▶ 直接 false(数据都不用看)
类型相同 + 指针形状 ──▶ 直接比数据字 x == y
其余 ──▶ 调 t.Equal(数据1, 数据2)
"含 slice 的 struct 不可比较"的完整因果链 :slice 没有判等规则 → 编译器不为含 slice/map/func 的类型生成 Equal → t.Equal == nil。静态类型直接写 == 是编译错误;装进 any 再比,走到 panic("comparing uncomparable type ...")(alg.go:374);当 map[any]V 的 key 同理 panic("hash of unhashable type",alg.go:144)。Comparable() 看的就是这个字段空不空。
PtrToThis:"*T 的档案放在哪"
只要程序里出现类型 T,几乎立刻会出现 *T,所以编译器生成 T 的元数据时通常顺手把 *T 的也生成 ,并在 T 上记一笔:"*T 的档案在从类型区起点数的第 N 个字节"------存相对偏移(int32,比存指针省一半内存),没有现成的记 0。
纠正:reflect.TypeOf(&x) 用不到它------&x 装箱时编译器直接把 *T 的元数据地址写进 interface。真正受益的是反方向 :reflect.PointerTo(t)(老名 PtrTo),从 T 反手要 *T。查找顺序(reflect/type.go:1329):
javascript
if at.PtrToThis != 0 { // ① 档案现成:按偏移一步取,零成本
return t.typeOff(at.PtrToThis)
}
if pi, ok := ptrMap.Load(t); ok { ... } // ② 查动态构造的缓存
for _, tt := range typesByString("*"+t.String()) { ... } // ③ 全局搜
// ④ 都没有才现场造(新造的 PtrToThis = 0,type.go:1358)
再补一层:这个"偏移"到底是什么?
PtrToThis 不是"当前类型的指针偏移量"。断句是:指向 当前类型 的指针类型(*T)的元数据位置 ------存的不是地址,是偏移量。名字读法:Ptr-to-this = "pointer to this type" = *T。整句翻译:"如果以后要找 *T 的档案,从类型区起点往后走 PtrToThis 这么多字节就是它。"
它链接的是两份类型元数据 ,跟值无关(跟 &x、跟任何具体数据都没关系):
ini
模块类型区起点(moduledata.types)
│
├─ 偏移 0x200 ──▶ Foo 的元数据 Kind_ = Struct
│ └ PtrToThis = 0x500 ──┐
│ │ 从起点走 0x500 字节
├─ 偏移 0x500 ──▶ *Foo 的元数据 ◀─────┘ Kind_ = Pointer
└ Elem ────── 指回 Foo(反向链接)
两个排除项:不是"T 自己在哪"的偏移(记自己没意义,都是别人链到你);不是值层面的指针。纯粹是元数据之间的高速通道 ,reflect.PointerTo(t) 一发命中就靠它。
为什么存偏移不存指针 :TypeOff 是 int32(abi/type.go:133),比 8 字节指针省一半内存,每个类型都有一项、几十万个类型积少成多;更关键的是程序加载地址不固定(ASLR)、各模块(含插件/动态库)各自有类型区,写死绝对地址要链接器逐个重定位,存"相对本模块类型区起点的偏移"则加载到哪都能算------运行时 resolveTypeOff 现场加一下基准地址即可。
"可为 0" = 没有现成的 *T 档案。编译器生成的类型基本都有;reflect.SliceOf/StructOf/PointerTo 运行时现场造的类型没人预生成过 *T,造出来时 PtrToThis = 0,下次要 *T 只能走缓存→搜索→再造的降级链。
同款设计:旁边的 Str NameOff 存"类型名字符串相对类型区起点的偏移"------NameOff 管"名字字符串在哪",TypeOff 管"另一份类型元数据在哪"。
2. 复合类型的元数据布局:公共头 + 私有尾
所有类型元数据都是 abi.Type 公共头 + 各自私有字段 的结构。reflect 通过unsafe.Pointer 强转来解读"尾部":
go
// reflect/type.go(示意,字段名以 1.24 为准)
type arrayType = abi.ArrayType // 头 + Elem *Type + Slice *Type(切片视角) + Len
type chanType = abi.ChanType // 头 + Elem *Type + Dir
type funcType = abi.FuncType // 头 + InCount/OutCount,参数类型表内联紧跟在档案尾部
type mapType = abi.MapType // 头 + Key/Elem *Type + ...
type ptrType = abi.PtrType // 头 + Elem *Type
type sliceType = abi.SliceType // 头 + Elem *Type
type structType = abi.StructType // 头 + PkgPath + Fields []StructField(1.24 是真 slice)
所有类型元数据都是 abi.Type 公共头 + 各自私有尾。核心图景:每个类型的"档案袋" = 统一封面 + 各自的附件页。
python
低地址
┌────────────────────────────┐
│ 封面(abi.Type,48字节) │ ← 所有档案袋的封面
│ 大小/哈希/Kind章/Equal/GC位图 │ 排版完全一样
├────────────────────────────┤
│ 附件页(因 Kind 而异) │ ← 翻开封面才知道附了什么
│ 数组: Elem/Slice/Len │
│ 指针: Elem │
│ 结构体: 字段清单 │
└────────────────────────────┘
为什么必须有统一封面
runtime 每天海量处理各种类型的值,但问的问题 90% 是通用问题,只看封面就能答:
- 分配内存:"多大?对齐多少?" →
Size_/Align_ - GC:"哪些字节是指针?" →
PtrBytes/GCData - interface 装箱:"是哪种?怎么比相等?" →
Kind_/Hash/Equal
关键机关:Kind 章盖在封面上。拿到任意档案,读一个字节就知道"要不要翻附件、翻哪一页"。
附件页放什么(go1.24 实际字段,internal/abi/type.go:275-568)
| Kind | 附件页 | 大白话 |
|---|---|---|
| 指针 | Elem |
我指向谁 |
| 切片 | Elem |
我的元素是谁 |
| 数组 | Elem+Slice+Len |
元素是谁、长度;顺手把 []T 的档案也链上,a[:] 直接用 |
| chan | Elem+Dir |
传什么、哪个方向 |
| func | InCount/OutCount + 紧跟其后的参数类型表 |
几进几出、各是什么 |
| map | Key+Elem+哈希函数等(abi/map.go) |
键值各是什么、键怎么算哈希 |
| struct | PkgPath+Fields []StructField |
我由哪些字段拼成 |
附件页记录的全是跳向其他档案的链接(和 PtrToThis 一样,元数据互相链成一张网)。
注意与旧版差异(1.24 为准):StructType.Fields 是真 slice([]StructField),不是内联数组;FuncType 的参数类型表才是"紧跟档案尾部"的内联布局(abi/type.go:490-505 注释里画了图)。
unsafe.Pointer 强转为什么合法
所有 XxxType 都把 Type 内嵌在第一个字段 (偏移 0),所以 *abi.Type、*abi.ArrayType、*abi.StructType 指向同一块内存的同一地址 ,封面完全重叠。官方代码自己就用这招(abi/type.go:283):
scss
func (t *Type) Len() int {
if t.Kind() == Array {
return int((*ArrayType)(unsafe.Pointer(t)).Len)
}
return 0
}
"强转"不复制、不转换数据------只是给同一个地址换个"镜头",继续往后读几个字节。安全前提:先读封面 Kind 再决定换哪个镜头。
struct 的字段清单:Field(i) 的本质:
go
type StructField struct {
Name Name // 字段名编码块:首字节位标志(导出?嵌入?有tag?)+ 名字 + tag
Typ *Type // 字段类型档案在哪(链接)
Offset uintptr // 字段数据离结构体开头几个字节
}
v.Field(i) 白话流程:翻到字段清单第 i 行 → 数据在哪 = 结构体起点 + Offset → 怎么解读 = Typ 指向的档案 → 名字/tag/导出没 = 解码 Name → 未导出就给 flag 打 flagStickyRO(呼应前文 flag 表)。
sql
type User struct{ Name string; Age int }
// Field(0): {Name:"Name", Typ:string档案, Offset:0}
// Field(1): {Name:"Age", Typ:int档案, Offset:16}
json.Marshal 遍历字段、Tag.Get("json") 查 tag,都是从 Name 编码块里解出来的。
为什么这样设计(三个大白话理由):
- 统一收发:封面统一,GC/分配器/interface 这些高频路径一份代码通吃所有类型,分派只靠封面一个字节。
- 零冗余:int 要什么 Elem?func 要什么 Offset?各存各的,没有空字段。若做成"全字段大杂烩 struct",几十万个类型每人背一身用不上的空位,体积和内存爆炸。
- 天生只读、可并发:编译期生成、链接期拼好、运行期不改,所以能放只读段、随意并发读不加锁------reflect 敢到处裸读这些结构的底气。
3. Name:被极致压缩的名字编码
Name(定义在 internal/abi/type.go:570,没有独立的 name.go)不是普通 string,而是一段紧凑字节数组:首字节是位标志,后面一次编码存了:名字本身、是否导出、是否嵌入字段、tag 内容。
解码入口是 abi.Name 的方法(1.24 中 reflect 不再有自己的私有 name 类型,直接用 abi 的):
go
// internal/abi/type.go:613-676
func (n Name) IsExported() bool // 首字节 1<<0 位
func (n Name) IsEmbedded() bool // 首字节 1<<1 位(嵌入字段)
func (n Name) HasTag() bool // 首字节 1<<2 位
func (n Name) Name() string
func (n Name) Tag() string // tag 原始字符串;解析成 key/value 是 reflect.StructTag 的活
这也解释了 StructTag.Get(第三篇详述)为什么是字符串解析而非查表------tag 在元数据里就是内联字符串,运行时第一次 Get 时才做切分。
名字和 tag 用 相对模块基址的偏移(NameOff/TypeOff) 存储,配合 resolveNameOff/resolveTypeOff(由 runtime linkname 提供,type.go:497-509)还原成指针。好处:链接器只需重定位整数偏移,不用对每个字符串做地址重定位,二进制更小。
abi.Name ------ 把"名字 + tag + 权限"压进一个指针的极致编码:
问题源头: StructField 里有一句注释------"字段名编码块:首字节位标志(导出?嵌入?有tag?)+ 名字 + tag",当时一句话带过。这个"编码块"的真身就是 abi.Name(internal/abi/type.go:596):
go
// Name is an encoded type Name with optional extra data.
type Name struct {
Bytes *byte
}
整个结构体只有一个指针字段。它不是"名字"本身,而是一根指向一段"自描述字节流"的指针------名字、tag、是否导出、是否嵌入字段、包路径,全部打包编码在那串字节里,用到哪段现场解哪段。
A. 字节流布局:一张"自描述"的折叠纸条
官方注释画了格式(abi/type.go:570-594),展开成图:
ini
Bytes ──▶ ┌────────┬──────────┬───────────┬────────────┬────────────┬────────────────┐
│ 标志字节 │ 名长varint │ 名字字节... │ [tag长varint]│ [tag字节...] │ [包路径偏移4字节] │
└────────┴──────────┴───────────┴────────────┴────────────┴────────────────┘
必有 必有 必有 仅当bit1=1 仅当bit1=1 仅当bit2=1
标志字节是个 8 位开关板:
| 位 | 含义 | 读取方法(都在 abi 包内) |
|---|---|---|
| bit0 | 名字是否导出(大写开头) | IsExported():读一个字节做与运算(type.go:613) |
| bit1 | 后面跟不跟 tag | HasTag()(type.go:618) |
| bit2 | 末尾跟不跟包路径偏移 | 只有 reflect 的 pkgPath() 手工解(reflect/type.go:410-426) |
| bit3 | 是不是嵌入(匿名)字段 | IsEmbedded()(type.go:623) |
一个特殊规则(type.go:589):若名字以 * 开头,bit0 表示的是被指向的类型 是否导出(比如 *template.Template,template 包未导出)。
三个压缩手段一次用全:
- varint 长度 :与
encoding/binary兼容(ReadVarint,type.go:629)。名字几乎都短于 128 字节 → 长度只占 1 字节; - 可选段不占位:没有 tag 就用 1 个 bit 说清"无",0 字节开销。绝大多数字段没有 tag;
- 包路径存偏移不存字符串:末尾 4 字节 nameOff 指回模块类型区,同包几十个字段共享同一份路径字符串。
拿真实例子看字节(type User struct{ Name string json:"name"; age int }):
c
字段 Name string `json:"name"` 字段 age int
Bytes ▼ Bytes ▼
03 04 4E 61 6D 65 0B 6A 73 6F 6E 3A 22 6E 61 6D 65 22 00 03 61 67 65
│ │ └─ N a m e ─┘ │ └──── j s o n : " n a m e " ───┘ │ └ a g e ┘
│ └ 名长=4 └ tag长=11 │ └ 名长=3
└ 标志=导出(bit0)+有tag(bit1) └ 标志=全0(未导出、无tag)
解码就是顺流而行。Name()(type.go:665):
go
func (n Name) Name() string {
i, l := n.ReadVarint(1) // 从第 1 字节读 varint:i=占用字节数, l=名字长度
return unsafe.String(n.DataChecked(1+i, "non-empty string"), l)
} // ↑ unsafe.String:零拷贝!直接拿只读段里的字节当 string,不分配、不复制
Tag()(type.go:674)接着跳:名字段结束后(偏移 1+i+l)再读一个 varint,得到 tag 长度,再 unsafe.String 一次。n.Bytes == nil 是合法零值,一切读取返回空串(比如内置类型 int 的 PkgPath)。
B. 用在反射的哪些地方(总表)
两种持有形式:直接持有 Name (结构里嵌 8 字节指针),或存 NameOff (4 字节相对偏移,用时经 resolveNameOff 解析成 Name)。谁用哪种、供出什么:
| 持有者 | 形式 | 供出的信息 | 你调用的公开 API |
|---|---|---|---|
abi.Type.Str |
NameOff | 类型全名 | Type.String() / Type.Name()(reflect/type.go:569、693) |
abi.StructField.Name |
Name | 字段名、tag、导出、嵌入 | Type.Field(i)(type.go:1102) |
abi.StructType.PkgPath |
Name | 结构体的包路径 | Type.PkgPath() |
abi.InterfaceType.PkgPath |
Name | 接口的包路径 | Type.PkgPath() |
abi.UncommonType.PkgPath |
NameOff | 命名类型的包路径 | Type.PkgPath()(type.go:686) |
abi.Method.Name |
NameOff | 方法名(方法表里每项) | Type.Method(i)(type.go:622) |
abi.Imethod.Name |
NameOff | 接口方法名 | Type.Method(i) 接口分支(type.go:968) |
外加两个隐蔽使用点:FieldByName 按名字找字段时逐个比对 tf.Name.Name() == name(type.go:1286),字段匹配还要连 tag 一起比(type.go:1618-1624);StructOf 拼新类型时把 tag 字节搅进缓存哈希(type.go:2330)。
C. 四个代表场景逐一看
场景1:Type.String() ------ 类型名的读取与 * 前缀复
go
func (t *rtype) String() string { // reflect/type.go:569
s := t.nameOff(t.t.Str).Name()
if t.t.TFlag&abi.TFlagExtraStar != 0 { // 存的名字带一个多余的 '*'
return s[1:]
}
return s
}
nameOff(type.go:553)把偏移交给 runtime 的 resolveNameOff(runtime/type.go:290):拿"这个类型档案自己所在的地址"当锚点,找到它属于哪个模块的类型区 [md.types, md.etypes),然后 md.types + off 一加就是目标地址。存偏移不存指针 :int32 比指针省一半,且加载地址随机(ASLR)、插件各有类型区时天然免重定位------和附2 PtrToThis 同一套哲学。
TFlagExtraStar 则是字节流层面的省法:程序里出现 T 几乎必然出现 *T,两条档案共用同一份字节流 *main.User------*User 的档案直接读它(标志位不设),User 的档案指向同一串字节并设 TFlagExtraStar,读出来掐掉首字符。一份字节喂两个类型。
场景2:Field(i) ------ 一个 Name 供出四样信息(reflect/type.go:1102-1115)
scss
p := &t.Fields[i]
f.Name = p.Name.Name() // 字段名
f.Anonymous = p.Embedded() // → Name.IsEmbedded():读 bit3
if !p.Name.IsExported() { // 读 bit0
f.PkgPath = t.PkgPath.Name() // 未导出字段才需要包名限定
}
if tag := p.Name.Tag(); tag != "" { // 读 bit1 → 解出 tag
f.Tag = StructTag(tag) // json.Marshal / f.Tag.Get("json") 的源头
}
场景3:MethodByName ------ 方法表二分查找的现场解码
方法表 []abi.Method 每项的 Name 也是 NameOff。方法按名字典序存放,所以 MethodByName(type.go:658-673)敢用二分,每比一次现场解码一个方法名参与比较:
ini
if !(t.nameOff(methods[h].Name).Name() >= name) { i = h + 1 } else { j = h }
unsafe.String 的零分配特性让"比较现场造 string"这件事不至于太贵。value.go:880、897 两处也只读一下 IsExported 位来过滤方法可见性------一个字节的与运算,就是一次可见性判断的全部成本。
场景4:StructOf 动态造类型 ------ 堆上的名字怎么获得"合法偏移"
运行时 StructOf/FuncOf 拼新类型时,字段名靠 newName(→ abi.NewName,abi/type.go:683)现场编码成字节流(这次在堆上)。可 abi.Type.Str 这类字段要的是 NameOff,堆地址没有"模块偏移"怎么办?resolveReflectName(reflect/type.go:536-538)调 runtime 的 addReflectOff,把这串字节的地址注册进全局表 reflectOffs.m,拿一个 ID 当偏移用;resolveNameOff 发现锚点不属于任何模块时,就退回查这张表(runtime/type.go:306-309)。同一套偏移机制,静态(编译期 · 模块类型区)与动态(运行期 · 注册表)两路供给。
D. 为什么要压到这个地步
算笔账。二进制里每个类型、每个结构体的每个字段、每个方法 都要存名字元数据,中型程序轻松几十万个 Name。以 Name string json:"name"`` 为例对比:
| 存法 | 元数据开销 |
|---|---|
| 朴素:Name string头(16B) + Tag string头(16B) + bool×2 + 对齐填充 | 结构里 ~40B/字段,两串字符还各有独立分配开销 |
| abi.Name:StructField 内嵌 8B 指针 + rodata 里 18B 编码流 | 8B + 18B,编码流全程序去重、进只读段 |
五个动机总结:
- 体积 :Name 按值 嵌进每个
StructField,字段清单动辄几百项,8B 对 40B 再乘以全程序字段总数; - 零拷贝 :
unsafe.String让Name()/Tag()返回的 string 直接指向只读段的字节,不分配不复制------reflect 取字段名敢标榜零 alloc 的底牌; - 位置无关 :NameOff 是相对模块类型区起点的 int32 偏移,省一半空间且免重定位(同
PtrToThis); - 按需编码:varint 长度 + 可选段,"没有的信息"只花 1 个 bit;
- 只读共享:字节流编译期写好、链接期去重、躺进只读段,多进程 mmap 共享、并发读免锁(附3 第 3 条的底气)。
E. 谁写、谁读:三方契约
注释末段(type.go:592-594)点名:这份编码是编译器、链接器、运行时三方的共同契约,改一处必须同步另两处:
cmd/compile/internal/reflectdata/reflect.go------ 编译期写入;cmd/link/internal/ld/decodesym.go------ 链接期解读/去重;internal/abi的 Name 方法 + runtime/reflect ------ 运行期按需解码 (运行期造的新类型由abi.NewName补写)。
一个耐读的细节:DataChecked(off, whySafe) 的 whySafe 参数不是注释摆设------若哪天指针算术越界引发崩溃,这个字符串会出现在 crash 栈里,说明"为什么当初认为这次加法是安全的"(abi/type.go:600-610)。
F. 大白话重讲
把 abi.Name 想成行李牌上只写了一个储物柜号,柜子里是一张折好的自描述纸条:
- 纸条第一行是目录(标志字节):导出没?附没附 tag?附没附包路径?是不是嵌入字段?------四个问题一行的空间答完;
- 第二行起是正文:名字长度(能一个字节就一个字节)+ 名字本尊;
- 目录说有 tag,后面才跟 tag 段;说有包路径,末尾才补 4 字节"去类型区第 N 字节找";
- 拿到 Name 的人要什么解什么:只问"导出没"就只读第一字节做一次与运算;要 tag 才顺着目录多走一段。没有人把它"整个展开成 struct"------展开就失去体积优势了。
一句话总结:abi.Name = 一根指针 + 一段自描述字节流,把"名字类元数据"压到极限,换来二进制体积、零拷贝读取、位置无关三重收益;你调用的每个与"名字"有关的反射 API------Type.String/Name/PkgPath、Field(i) 的 Name/Tag/Anonymous、Method/MethodByName------底层都是在解这串字节。
4. 方法表与 uncommonType
方法信息放在可选的 UncommonType(type uncommonType = abi.UncommonType):
go
type UncommonType struct {
PkgPath NameOff
Mcount uint16
Xcount uint16
Moff uint32
_ uint32
}
// 方法表项:abi.Method 四元组 { Name NameOff; Mtyp TypeOff; Ifn TextOff; Tfn TextOff }
// (abi/type.go:224,Ifn 是接口调用入口、Tfn 是普通调用入口)
// 按名字排序存储:先导出方法(参与 interface 满足),后未导出方法
关键规则:
NumMethod只统计导出方法 ------ 这正是 interface 满足判断的基础。- 方法的 receiver 可能是值或指针:
-
- 值类型方法表:值方法。
- 指针类型方法表:值方法 + 指针方法(指针可读值)。
- 所以
T的方法集 ⊆*T的方法集,var i I = &t{}比= t能满足更多 interface。
uncommon()通过 TFlag 判断是否存在,不存在返回 nil(如内建 int)。
Method(i) 的返回结构:
go
type Method struct {
Name string
PkgPath string
Type Type // 方法签名(不含 receiver)
Func Value // 方法本体(第一参数为 receiver 的函数)
Index int
}
方法元数据------UncommonType、abi.Method 四元组与 Method(i) 的名片:
A. 方法表挂在哪:封面 + 附件页 + UncommonType + 方法数组:
go
// internal/abi/type.go:235-241
type UncommonType struct {
PkgPath NameOff // import path; empty for built-in types
Mcount uint16 // 全部方法数(含未导出)
Xcount uint16 // 导出方法数(= reflect 的 NumMethod)
Moff uint32 // 从本 UncommonType 到 [Mcount]Method 数组的偏移
_ uint32
}
// type.go:224-229 ------ 方法表项四元组
type Method struct {
Name NameOff // 方法名(附4 的 Name 编码)
Mtyp TypeOff // 方法签名(不含 receiver)
Ifn TextOff // fn used in interface call (one-word receiver) ------ 一字接收者包装入口
Tfn TextOff // fn used for normal method call ------ 普通调用入口
}
挂载方式与上面 一脉相承:封面 + 附件页 + u + 方法数组 。abi.(*Type).Uncommon()(type.go:325-381)先看 TFlag&TFlagUncommon,再按 Kind 决定 u 在哪种附件页之后------Struct/Pointer/Func/... 各有 {XxxType; u} 布局,default 分支是 {Type; u} (int/string 这类只有封面的类型,u 紧跟 48 字节封面)。方法数组不必紧贴 u------用 Moff 从 &u 往后找(Methods(),type.go:243)。排序规则:导出方法按名字典序排前 [0..Xcount),未导出跟在后面。
B. Method(i) 五字段怎么填(reflect/type.go:612-644)
| 字段 | 从方法表哪来 | 填法 |
|---|---|---|
Name |
p.Name(NameOff) |
t.nameOff(p.Name).Name()------解附4 的 Name 编码 |
PkgPath |
u.PkgPath |
只对未导出方法填 (限定小写名字属于哪个包);导出为空串,IsExported() 就是判它 |
Type |
p.Mtyp(TypeOff) |
现场新造 :把 Mtyp 的 funcType(不含 receiver)拆开、把 t 自己拼成第一个参数、FuncOf 重装(:627-636)→ 具体类型的 Type 是 func(main.User) string带 receiver |
Func |
p.Tfn(TextOff) |
Value{mt, &tfn, flag(Func)}(:638-640)------里面躺的就是 Tfn 代码入口 |
Index |
--- | 下标 i |
接口走另一分支(:613 → interfaceType.Method):Imethod 只有 {Name, Typ} 两元组------Type 是 func() string不带 receiver ,Func 是零值。接口名册只有"岗位描述"没有电话,电话在具体类型的方法表里,itab 配对时才抄过去(附7 的 writeITab 求交集,抄的正是导出段的 Ifn)。
注意区分:Type.Method(i).Func 是未绑定 的函数值(flag 只有 Func,调用时第一个参数传 receiver);Value.Method(i) 才是绑定 receiver 的 method value(打 flagMethod + 方法编号,呼应 Q1 的 bit9/bit10+)。
C. 实测对账(数字为单次运行值,只有关系是稳定的)
go
type User struct{ Name string }
func (u User) Greet() string // 值接收者 · 导出
func (u User) whisper() int // 值接收者 · 未导出
func (u *User) SetName(n string) // 指针接收者 · 导出
ini
== User == Kind=25 TFlag=0x07 Mcount=2 Xcount=1 Moff=40
[0] Greet Ifn=970464 Tfn=964960 ← Ifn≠Tfn(值接收者要包装)
[1] whisper Ifn=-1 Tfn=-1 ← 未导出 = 哨兵值!
== *User == Kind=22 TFlag=0x09 Mcount=3 Xcount=2 Moff=16
[0] Greet Ifn=Tfn=970464 ← 正是 User.Greet 的 Ifn!
[1] SetName Ifn=Tfn=965056 ← 指针接收者本来一字,无需包装
[2] whisper Ifn=-1 Tfn=-1
== int == Kind=2 TFlag=0x0f Mcount=0 Xcount=0 ← 有 u,但是空表!
== struct{X int }(匿名) == TFlag=0x0a UncommonType: nil ← 真正的 nil 例子
== 公开 API ==
User.NumMethod=1(=Xcount) *User.NumMethod=2
rt.Method(0): Type="func(main.User) string",Func.Call(receiver 在第一个参数)→"hi Tom"
Greeter.Method(0): Type="func() string",Func 是零值
rt.Method(0).Func.Pointer() == reflect.ValueOf(User.Greet).Pointer() == 0x19c960
五个字节级发现:
NumMethod == Xcount:User 公开只有 1 个方法,但Mcount=2------whisper 躺在表里,反射永远看不见。T 方法集 ⊆ *T 方法集在字节层面可见 :*User的表是 3 行(Greet 的指针包装 + SetName + whisper),User 的表 2 行。- Ifn/Tfn 一份数据两处引用 :User.Greet 的 Tfn=970464(一字接收者包装)、Ifn=964960(原始入口)------而
*User表里 Greet 的 Tfn 就是 970464:编译器给*T方法集生成的包装函数,正是T表里的 Ifn。 - 未导出方法
Ifn=Tfn=-1:不参与接口满足(itab 只抄导出段)、反射不可见,入口都不登记;u.whisper()直接调用是静态调用,根本不查表。 - 对账闭环 :
Method(0).Func.Pointer() == reflect.ValueOf(User.Greet).Pointer()------Func 里躺的就是 Tfn。
TFlag 顺带解码(呼应附2 / 验证三):User=0x07(Uncommon|ExtraStar|Named),*User=0x09(Uncommon|RegularMemory,无 Named 无 ExtraStar),int=0x0f(四项全有),匿名 struct=0x0a(ExtraStar|RegularMemory,无 bit0)。
Moff 的用途也实测出来了 :User 的 Moff=40,u(16 字节)与方法表之间夹着 24 字节------探针显示是 Fields 的底层数组 ({Name指针, Typ指针, Offset},第一字按附4 编码解出正是 "Name")。方法数组在 blob 尾部灵活排版,Moff 就是"跨过中间夹的变长数据"找到它的尺子;*User 没有字段数组,所以 Moff=16 紧跟其后:
ini
User 元数据 blob 布局(实测):
┌ 封面 abi.Type 48B(Kind_=25, TFlag=0x07)
├ 附件页:PkgPath(8B) + Fields 切片头(24B)
├ u UncommonType 16B(Mcount=2 Xcount=1 Moff=40)
├ 24B:Fields 底层数组 [{Name:"Name", Typ:string档案, Offset:0}] ← Moff 跳过它
└ 方法表 @&u+40:[0]Greet [1]whisper
Moff 的取数公式只有一条:methods = (*[Mcount]Method)(&u + Moff)------官方门面 Methods()/ExportedMethods()(abi/type.go:243/250,后者同一数组切 [:Xcount])、运行期 itab 配对 additab(runtime/iface.go:215 的 add(x, x.Moff) 就是这把尺的现场使用)都靠它。Moff≥16 恒成立(16 = u 头自身长度 = 紧贴的最小值);它是编译期直写的纯常数(附9 B 表),锚点是 u 自己而非模块类型区------方法数组永远与 u 同住一个 blob,局部锚点最近路(与 NameOff/PtrToThis 的全局锚同族不同锚)。
E. 大白话重讲:员工名册
把 UncommonType 想成类型档案袋最后钉上的员工名册:
- 名册分两段 :前
Xcount行是对外营业的 (导出方法,按名字排序),后面是内部员工 (未导出,连电话都懒得登记,登记栏写 -1)。Mcount是全册行数; - NumMethod 只数营业行 ;
MethodByName的二分查找也只翻营业段(所以找不到 whisper); - *T 的名册 = T 的营业行换上"转发分机"抄一遍 + 指针方法 ------所以
&t能应答的总机(接口)比t多; Method(i)就是按名册抄一张名片:名字(Name)、岗位职责(Type------reflect 特意把"你得先把 receiver 递给我"写进职责第一行)、直拨电话(Func,拨通即用,第一个参数传 receiver)、名册行号(Index);- 接口的名册只有岗位描述没有电话(Imethod 两元组)------真要打电话得等具体类型来应聘(itab 配对),总机把它的电话抄进自己的 Fun 表;
- Ifn/Tfn 两种电话:Tfn 是直线 (接收者按声明方式传),Ifn 是总机转接线 (接收者压成一个字)------值接收者方法两条线不同(
*T表里那条"转发分机"就是它),指针接收者本来就是一字,两条线并成一条。
5. 三个判断函数的源码(type.go:1385-1410):
scss
func (t *rtype) Implements(u Type) bool { // type.go:1385
if u == nil {
panic("reflect: nil type passed to Type.Implements")
}
if u.Kind() != Interface {
panic("reflect: non-interface type passed to Type.Implements")
}
return implements(u.common(), t.common())
}
func (t *rtype) AssignableTo(u Type) bool { // type.go:1395
if u == nil {
panic("reflect: nil type passed to Type.AssignableTo")
}
uu := u.common()
return directlyAssignable(uu, t.common()) || implements(uu, t.common())
}
func (t *rtype) Comparable() bool { // type.go:1410
return t.t.Equal != nil
}
implements(同文件)的核心逻辑:
- 拿到接口类型的
[]imethod(接口方法签名列表,只含导出方法); - 拿到具体类型(或其指针类型)的方法表;
- 双指针从前往后扫:两边都按方法名排序,名字相同再比签名(签名相同即类型指针相同);
- 接口方法全部匹配上 → true。
这是 O(N+M) 的归并扫描,没有缓存(类型满足关系编译期已保证,运行时只在断言处即时计算;interface 断言的 itab 缓存由 runtime 管理,不在 reflect)。
directlyAssignable 判定要点(读源码总结):
- 类型指针相同 → true(同一类型);
- 目标是 interface →
implements; - channel 方向兼容、命名类型与底层类型相同且至少一方未命名等特判;
- 其余 false。
ConvertibleTo比AssignableTo多一张"显式转换表"(数值互转、string↔\[\]byte 等,见源码大 switch)。
6. Type 接口常用方法速查
| 方法 | 作用 | 备注 |
|---|---|---|
Kind() |
基础种类 | 27 种枚举 |
Name()/String() |
名字/完整表示 | unnamed type 的 Name 为空串,String 为 struct { X int } |
PkgPath() |
包路径 | 内建类型为空 |
Size()/Align() |
大小/对齐 | 直接读 rtype 字段 |
NumMethod()/Method(i) |
方法表 | 只含导出方法 |
Implements/AssignableTo/ConvertibleTo |
判断 | 见第 5 节 |
Elem() |
元素类型 | Ptr/Slice/Map/Array/Chan/Func 均适用 |
NumField()/Field(i) |
struct 字段 | 第三篇详述 |
Key() |
map 的 key 类型 | |
IsVariadic() |
函数是否变参 | 变参类型是 []T 切片 |
abiName示例:
scss
package abiName
import (
"bytes"
"fmt"
"reflect"
)
// ---- 演示用类型:名字/tag/导出性在编译期就被编码进 rodata ----
type User struct {
Name string `json:"name"`
age int
}
func (u User) Greet() string { return "hi " + u.Name }
func (u User) whisper() int { return u.age } // 未导出:不进导出方法表
type Greeter interface {
Greet() string
}
type inner struct{ X int }
type Outer struct {
inner // 未导出嵌入字段:bit3=1 且 bit0=0 → flagEmbedRO 场景
Score float64 `json:"score"`
}
var failed int
func ok(cond bool, msg string) {
if cond {
fmt.Printf(" ✅ %s\n", msg)
} else {
failed++
fmt.Printf(" ❌ %s\n", msg)
}
}
// Demo 依次跑完六个验证,可在最外层 main.go 里调用。
func Demo() {
failed = 0
DemoFieldBytes()
DemoZeroCopy()
DemoExtraStar()
DemoFieldRO()
DemoMethodNames()
DemoEncodeRoundTrip()
fmt.Println()
if failed == 0 {
fmt.Println("========== 全部验证通过 ✅ ==========")
} else {
fmt.Printf("========== 有 %d 项验证未通过 ❌ ==========\n", failed)
}
}
// 验证一:亲手读出结构体字段的 Name 字节流(附4 A 节的 hex dump 对账)
func DemoFieldBytes() {
fmt.Println("\n===== 验证一:结构体字段的 Name 原始字节(编译期写好、躺在只读段) =====")
rt := reflect.TypeOf(User{})
st := structTypeOf(rt)
for i := range st.Fields {
f := &st.Fields[i]
fmt.Printf("[字段 %d] 编码字节共 %d 个: % X\n", i, len(f.Name.raw()), f.Name.raw())
fmt.Printf(" 镜像解码 : name=%q tag=%q exported=%v embedded=%v\n",
f.Name.name(), f.Name.tag(), f.Name.isExported(), f.Name.isEmbedded())
pub := rt.Field(i)
fmt.Printf(" 公开 API : name=%q tag=%q exported=%v anonymous=%v pkgPath=%q\n",
pub.Name, pub.Tag, pub.IsExported(), pub.Anonymous, pub.PkgPath)
ok(f.Name.name() == pub.Name, "镜像解码的名字 == reflect.Field(i).Name")
ok(f.Name.tag() == string(pub.Tag), "镜像解码的 tag == reflect.Field(i).Tag")
ok(f.Name.isExported() == pub.IsExported(), "bit0(导出位) == IsExported()")
}
}
// 验证二:零拷贝------我们的解码与 reflect 公开 API 返回的 string 指向同一片 rodata 字节
func DemoZeroCopy() {
fmt.Println("\n===== 验证二:零拷贝------unsafe.String 直读,不分配不复制 =====")
st := structTypeOf(reflect.TypeOf(User{}))
decoded := st.Fields[0].Name.name() // 我们用 unsafe.String 现场造的
fromReflect := reflect.TypeOf(User{}).Field(0).Name // reflect 公开 API 给的
fmt.Printf(" 镜像解码 Name() 的数据指针 = %p\n", strData(decoded))
fmt.Printf(" reflect Field(0).Name 的数据指针 = %p\n", strData(fromReflect))
ok(strData(decoded) == strData(fromReflect),
"两个 string 数据指针相同 → 都是 rodata 直读,Name()/Tag() 零分配的底牌")
}
// 验证三:TFlagExtraStar ------ T 与 *T 共用同一份名字字节流
func DemoExtraStar() {
fmt.Println("\n===== 验证三:User 与 *User 共用一份名字字节(TFlagExtraStar) =====")
tu := rtypeOf(reflect.TypeOf(User{})) // User 的封面
tp := rtypeOf(reflect.TypeOf(&User{})) // *User 的封面
fmt.Printf(" User : Kind=%d TFlag=0x%02x Str=%d String()=%q\n",
tu.Kind_&kindMask, tu.TFlag, tu.Str, reflect.TypeOf(User{}).String())
fmt.Printf(" *User : Kind=%d TFlag=0x%02x Str=%d String()=%q\n",
tp.Kind_&kindMask, tp.TFlag, tp.Str, reflect.TypeOf(&User{}).String())
ok(tu.Kind_&kindMask == 25, "User 的 Kind 低 5 位 = 25 (Struct)")
ok(tp.Kind_&kindMask == 22, "*User 的 Kind 低 5 位 = 22 (Pointer)")
ok(tu.TFlag&tflagNamed != 0, "User 是命名类型(TFlagNamed)")
ok(tp.TFlag&tflagExtraStar == 0, "*User 不带 ExtraStar:字节流里的 \"*abiName.User\" 直接就是它的名字")
ok(tu.TFlag&tflagExtraStar != 0, "User 带 ExtraStar:读同一份字节流,掐掉首字符 '*' 得 \"abiName.User\"")
ok(tu.Str == tp.Str, "两者 Str 偏移相等 → 同一份字节流喂两个类型(Str 是相对模块类型区的偏移,无法在用户态换成指针,但\"相等\"足以证明共享)")
}
// 验证四:Name 的 bit0/bit3 如何变成 Value flag 里的只读封条(呼应 01b 文档 Q1)
func DemoFieldRO() {
fmt.Println("\n===== 验证四:bit0/bit3 → flagStickyRO / flagEmbedRO 只读封条 =====")
st := structTypeOf(reflect.TypeOf(Outer{}))
for i := range st.Fields {
f := st.Fields[i]
fmt.Printf(" Outer 字段%d %-8q bit0(导出)=%v bit3(嵌入)=%v\n",
i, f.Name.name(), f.Name.isExported(), f.Name.isEmbedded())
}
o := Outer{}
v := reflect.ValueOf(&o).Elem() // 可寻址(flagAddr)
ok(v.CanAddr(), "ValueOf(&o).Elem() 可寻址")
ok(v.Field(1).CanSet(), "导出字段 Score:可 Set")
ok(!v.Field(0).CanSet(), "未导出嵌入字段 inner:不可 Set(bit3=1&bit0=0 → flagEmbedRO,value.go:1283)")
u := User{}
vu := reflect.ValueOf(&u).Elem()
ok(!vu.Field(1).CanSet(), "未导出字段 age:不可 Set(bit0=0 → flagStickyRO,value.go:1285)")
ok(!reflect.ValueOf(u).Field(0).CanSet(),
"对照组:ValueOf(u) 不可寻址,导出字段也 CanSet=false(缺 flagAddr 而非 RO 位)")
}
// 验证五:方法表里的名字(Method.Name / Imethod.Name 也是这套编码,经 NameOff 引用)
func DemoMethodNames() {
fmt.Println("\n===== 验证五:方法名与接口方法名 =====")
rt := reflect.TypeOf(User{})
fmt.Printf(" User.NumMethod=%d(whisper 未导出,不进导出方法表)\n", rt.NumMethod())
m, found := rt.MethodByName("Greet")
ok(found && m.Name == "Greet", "MethodByName(\"Greet\") 命中(二分查找时逐个现场解码方法名比较)")
_, found2 := rt.MethodByName("whisper")
ok(!found2, "MethodByName(\"whisper\") 找不到(未导出方法对反射不可见)")
var g Greeter = User{}
gt := reflect.TypeOf(&g).Elem()
ok(gt.NumMethod() == 1 && gt.Method(0).Name == "Greet",
"接口方法名走 Imethod.Name(同一套编码、以 NameOff 引用),Method(0).Name=Greet")
}
// 验证六:编码器镜像与编译器"对账"------同一份格式规范、两个独立实现
func DemoEncodeRoundTrip() {
fmt.Println("\n===== 验证六:encodeName 复刻 dnameData/abi.NewName,往返 + 对账 =====")
blob := encodeName("Name", `json:"name"`, true, false)
fmt.Printf(" encodeName 产物 %d 字节: % X\n", len(blob), blob)
n := nameEnc{Bytes: &blob[0]}
ok(n.name() == "Name" && n.tag() == `json:"name"` && n.isExported(),
"自编自解往返一致(decode(encode(x)) == x)")
real := structTypeOf(reflect.TypeOf(User{})).Fields[0].Name.raw()
ok(bytes.Equal(blob, real),
"与编译器写给 Field(0) 的真实 rodata 字节逐字节相同 → 编译器/运行时/本镜像三者同一份契约")
var zero nameEnc
ok(zero.name() == "",
"零值 Name{Bytes:nil} 的 Name() 返回空串(如内置类型 int 的 PkgPath;真实 abi 的 Tag/IsExported 不查 nil,调用方保证非空------本镜像照抄此语义)")
}
go
// Package abiName 用"镜像结构 + unsafe"逐字节验证 internal/abi.Name 的名字编码。
//
// 为什么不直接 import internal/abi?
//
// Go 规定 internal/ 下的包只允许其父目录树内的代码导入,
// GOROOT/src/internal/abi 只有标准库自己能 import,用户代码禁止。
// 所以这里手工复刻一份与 go1.24.0 internal/abi/type.go 完全同构的镜像
// (runtime 包里也维护着一份独立拷贝,是同一个道理)。
// 镜像必须与真实布局逐字节对齐(amd64):
//
// abi.Type 48 字节(统一"封面")
// abi.StructType = Type(48) + PkgPath Name(8) + Fields slice(24)
// abi.StructField = Name(8) + Typ(8) + Offset(8)
// abi.Name = Bytes *byte(指向一段自描述编码字节流)
//
package abiName
import (
"encoding/binary"
"reflect"
"unsafe"
)
// ---- abi.Type 镜像(internal/abi/type.go:20,与 reflectStudy/main.go 里的 abiType 相同)----
type abiType struct {
Size_ uintptr
PtrBytes uintptr
Hash uint32
TFlag uint8 // 附加元数据标志位(见下方 tflag* 常量)
Align_ uint8
FieldAlign_ uint8
Kind_ uint8 // 低 5 位 = Kind 枚举;bit5 = KindDirectIface
Equal func(unsafe.Pointer, unsafe.Pointer) bool
GCData *byte
Str int32 // 类型名 NameOff:相对本模块类型区起点的偏移
PtrToThis int32 // *T 的元数据偏移
}
const (
tflagExtraStar uint8 = 1 << 1 // 名字带多余的 '*' 前缀(T 与 *T 共用一份字节流)
tflagNamed uint8 = 1 << 2 // 命名类型
kindMask uint8 = 1<<5 - 1
)
// ---- abi.Name / StructField / StructType 镜像 ----
// nameEnc 是 abi.Name 的镜像(internal/abi/type.go:596):
// 只有一个指针,指向"标志字节 + varint名长 + 名字 + [varint tag长 + tag] + [4字节包路径偏移]"。
type nameEnc struct{ Bytes *byte }
type structField struct {
Name nameEnc // 字段名编码块(直接持有,非偏移)
Typ *abiType // 字段类型档案
Offset uintptr // 字段数据离结构体开头的字节数
}
type structType struct {
Type abiType
PkgPath nameEnc
Fields []structField
}
// ---- 从 reflect.Type 取出 *abi.Type 封面 ----
// rtypeOf 拆开 reflect.Type 接口:e[0] 是 itab,e[1] 是数据字。
// reflect.Type 的动态类型始终是 *reflect.rtype,而 rtype 的第一个字段就是 abi.Type,
// 所以 e[1] 直接就是 *abi.Type(和 reflectStudy/main.go 里拆 eface 的做法等价)。
func rtypeOf(t reflect.Type) *abiType {
e := *(*[2]unsafe.Pointer)(unsafe.Pointer(&t))
return (*abiType)(e[1])
}
// structTypeOf 把 *abi.Type 换个"镜头"看成 *structType(封面重叠,附3 讲过的强转)。
func structTypeOf(t reflect.Type) *structType {
if t.Kind() != reflect.Struct {
panic("abiName: not a struct type: " + t.String())
}
return (*structType)(unsafe.Pointer(rtypeOf(t)))
}
// ---- Name 解码器镜像:与 internal/abi/type.go:600-681 的方法一一对应 ----
// data 与 abi.(*Name).DataChecked 同款指针算术(单表达式写法,vet 可接受)。
func (n nameEnc) data(off int) *byte {
return (*byte)(unsafe.Pointer(uintptr(unsafe.Pointer(n.Bytes)) + uintptr(off)))
}
func (n nameEnc) isExported() bool { return *n.Bytes&(1<<0) != 0 } // bit0:导出
func (n nameEnc) hasTag() bool { return *n.Bytes&(1<<1) != 0 } // bit1:后跟 tag
func (n nameEnc) isEmbedded() bool { return *n.Bytes&(1<<3) != 0 } // bit3:嵌入字段
// readVarint 镜像 abi.(*Name).ReadVarint(type.go:629),与 encoding/binary 兼容。
func (n nameEnc) readVarint(off int) (int, int) {
v := 0
for i := 0; ; i++ {
x := *n.data(off + i)
v += int(x&0x7f) << (7 * i)
if x&0x80 == 0 {
return i + 1, v
}
}
}
// name 镜像 abi.(*Name).Name(type.go:665):unsafe.String 零拷贝直读。
func (n nameEnc) name() string {
if n.Bytes == nil {
return ""
}
i, l := n.readVarint(1)
return unsafe.String(n.data(1+i), l)
}
// tag 镜像 abi.(*Name).Tag(type.go:674):跳过名字段再读一段。
func (n nameEnc) tag() string {
if !n.hasTag() {
return ""
}
i, l := n.readVarint(1)
i2, l2 := n.readVarint(1 + i + l)
return unsafe.String(n.data(1+i+l+i2), l2)
}
// blobLen 按编码规则走一遍,算出整段字节流长度(供 hex 转储用)。
// bit2 的 4 字节包路径偏移只出现在"跨包定义的具体方法名"里,字段名不会有,按位补齐即可。
func (n nameEnc) blobLen() int {
if n.Bytes == nil {
return 0
}
l := 1
i, nameLen := n.readVarint(1)
l += i + nameLen
if n.hasTag() {
i2, tagLen := n.readVarint(1 + i + nameLen)
l += i2 + tagLen
}
if *n.Bytes&(1<<2) != 0 {
l += 4
}
return l
}
// raw 取出整段编码字节(只读,别改------它们躺在二进制的 rodata 段里)。
func (n nameEnc) raw() []byte {
if n.Bytes == nil {
return nil
}
return unsafe.Slice(n.Bytes, n.blobLen())
}
// ---- Name 编码器镜像:与 abi.NewName / 编译器 dnameData 逐字节互为镜像 ----
//
// 编译器那一侧(cmd/compile/internal/reflectdata/reflect.go:223 的 dnameData)
// 连 varint 都直接用标准库 binary.PutUvarint,这里照抄同样的写法。
func encodeName(name, tag string, exported, embedded bool) []byte {
var nameLen [binary.MaxVarintLen64]byte
var tagLen [binary.MaxVarintLen64]byte
nameLenLen := binary.PutUvarint(nameLen[:], uint64(len(name)))
tagLenLen := binary.PutUvarint(tagLen[:], uint64(len(tag)))
var bits byte
l := 1 + nameLenLen + len(name)
if exported {
bits |= 1 << 0
}
if len(tag) > 0 {
l += tagLenLen + len(tag)
bits |= 1 << 1
}
if embedded {
bits |= 1 << 3
}
b := make([]byte, l)
b[0] = bits
copy(b[1:], nameLen[:nameLenLen])
copy(b[1+nameLenLen:], name)
if len(tag) > 0 {
tb := b[1+nameLenLen+len(name):]
copy(tb, tagLen[:tagLenLen])
copy(tb[tagLenLen:], tag)
}
return b
}
// strData 取 string 的数据指针(比较"零拷贝"时用)。
func strData(s string) unsafe.Pointer {
return (*(*struct {
data unsafe.Pointer
len int
})(unsafe.Pointer(&s))).data
}
struct示例:
scss
// Package main ------ 结构体反射学习示例(单文件,自包含)。
//
// 运行:go run ./reflectStudy/structdemo
//
// 六个验证对应文档六个知识点:
//
// [1] StructField 元数据:Type.Field 给档案 / Value.Field 给值句柄(reflect/type.go:1000)
// [2] Value.Field 权限位传播:flagStickyRO 粘每一层 vs flagEmbedRO 止于本层(reflect/value.go:1267)
// [3] StructTag:Get/Lookup 惰性解析、区分"无 key"与"空值"、格式错误静默跳过(type.go:1036/1047)
// [4] 嵌入与提升:NumField 不展开、FieldByName 沿嵌入链 BFS、同深度冲突丢弃、外层遮蔽
// [5] VisibleFields 摊平 + FieldByIndex 过 nil 指针:panic vs FieldByIndexErr
// [6] 综合实战:迷你 json 字段映射器(文档 §7:VisibleFields + tag + omitempty + "-")
package main
import (
"fmt"
"reflect"
"strings"
"unsafe"
)
// ---- 验证一用的类型 ----
type Doc struct {
Title string `json:"title" doc:"标题"` // 多 key:空格分隔的 key:"value" 对
Pages int
draft bool // 未导出:PkgPath 会被填上
}
// ---- 验证二:三种只读封条的对照 ----
type Inner struct{ X int } // 导出类型
type hidden struct{ X int } // 未导出类型(只能在本包当嵌入字段用)
type stickyRO struct{ h hidden } // ① 未导出"具名"字段 → flagStickyRO:粘住每一层
type embedRO struct{ hidden } // ② 未导出"嵌入"字段 → flagEmbedRO:止步于本层
type openRO struct{ Inner } // ③ 导出嵌入:无封条
// ---- 验证三:tag 的五种形态 ----
type TagDemo struct {
A string `json:"a" db:"col_a" validate:"required"` // 多 key
B string `json:"b,omitempty"` // 带选项
C string `json:""` // 有 key 但值为空
D string `json:badform` // 格式错误(vet 会警告,运行期静默跳过)
E string // 无 tag
}
// ---- 验证四:提升、冲突、遮蔽、深层路径 ----
type A1 struct {
Name string
UID int
}
type A2 struct {
Name string
Alias string
}
type OnlyDup struct { // 只有同深度的两个同名提升字段
A1
A2
}
type Shadow struct { // 外层直接字段遮蔽提升字段
A1
A2
Name string
}
type Grand struct{ Deep int }
type Mid struct{ Grand }
type Leaf struct{ Mid }
// ---- 验证五/六共用 ----
type Address struct {
City string `json:"city"`
Zip int `json:"zip"`
}
type PtrEmbed struct{ *Address } // 嵌入指针:提升字段要跨一次解引用
type Base struct {
ID int `json:"id"`
Tags string `json:"tags,omitempty"`
}
type JItem struct {
Base // 嵌入:id/tags 被摊平进来
Addr Address // 命名结构体字段:玩具不递归,整体打印 {...}
Score float64 `json:"score,omitempty"`
Secret string `json:"-"` // "-":跳过
note string // 未导出:跳过
}
var failed int
func ok(cond bool, msg string) {
if cond {
fmt.Printf(" ✅ %s\n", msg)
} else {
failed++
fmt.Printf(" ❌ %s\n", msg)
}
}
// mustPanic 期望 f 触发 panic(把 panic 现场变成可验证的教学输出)。
func mustPanic(where string, f func()) {
defer func() {
if r := recover(); r != nil {
fmt.Printf(" ✅ %s → panic: %v\n", where, r)
} else {
failed++
fmt.Printf(" ❌ %s 没有按预期 panic\n", where)
}
}()
f()
}
func main() {
demo1StructField()
demo2FlagRO()
demo3Tag()
demo4Promote()
demo5IndexNilPtr()
demo6MiniJson()
fmt.Println()
if failed == 0 {
fmt.Println("========== 全部验证通过 ✅ ==========")
} else {
fmt.Printf("========== 有 %d 项验证未通过 ❌ ==========\n", failed)
}
}
// ===== 验证一:Type.Field 给档案,Value.Field 给句柄 =====
func demo1StructField() {
fmt.Println("\n===== 验证一:StructField 元数据 vs Value 值句柄 =====")
d := Doc{Title: "reflect", Pages: 10, draft: true}
t := reflect.TypeOf(d)
for i := 0; i < t.NumField(); i++ {
f := t.Field(i)
fmt.Printf(" Field(%d): Name=%-6s PkgPath=%-6q Type=%-7s Tag=%-28q Offset=%-3d Index=%v Anonymous=%v\n",
i, f.Name, f.PkgPath, f.Type, string(f.Tag), f.Offset, f.Index, f.Anonymous)
}
ok(t.Field(2).PkgPath == "main",
"draft 未导出 → PkgPath=\"main\";导出字段的 PkgPath 是空串")
ok(unsafe.Offsetof(d.Pages) == t.Field(1).Offset,
"Offset 与 unsafe.Offsetof 对账:元数据里的偏移就是真偏移(Field 只做 ptr+offset 一个加法)")
// Value.Field 给的是"值句柄":指针进、Elem() 开、导出字段才能写(坑清单第 5 条)
v := reflect.ValueOf(&d).Elem()
v.Field(1).SetInt(421)
ok(d.Pages == 421, "Value.Field(1).SetInt(421) 写回了原变量(&d 进 + Elem() 开 = flagAddr)")
}
// ===== 验证二:权限位传播------sticky 粘到底,embed 止于本层 =====
func demo2FlagRO() {
fmt.Println("\n===== 验证二:flagStickyRO vs flagEmbedRO(value.go:1267 那 20 行) =====")
vs := reflect.ValueOf(&stickyRO{}).Elem()
ve := reflect.ValueOf(&embedRO{}).Elem()
vo := reflect.ValueOf(&openRO{}).Elem()
fmt.Printf(" ① 未导出具名字段 h : h.CanSet=%-6v h.X.CanSet=%v\n",
vs.Field(0).CanSet(), vs.Field(0).Field(0).CanSet())
fmt.Printf(" ② 未导出嵌入字段 hidden: hidden.CanSet=%-6v hidden.X.CanSet=%v\n",
ve.Field(0).CanSet(), ve.Field(0).Field(0).CanSet())
fmt.Printf(" ③ 导出嵌入 Inner : Inner.CanSet=%-6v Inner.X.CanSet=%v\n",
vo.Field(0).CanSet(), vo.Field(0).Field(0).CanSet())
ok(!vs.Field(0).CanSet() && !vs.Field(0).Field(0).CanSet(),
"① flagStickyRO 粘每一层:链式下钻只要一层未导出,整条路径只读")
ok(!ve.Field(0).CanSet() && ve.Field(0).Field(0).CanSet(),
"② flagEmbedRO 止于本层:hidden 整体不可换,但其导出子字段 X 可以写(提升访问是合法的)")
ok(vo.Field(0).CanSet() && vo.Field(0).Field(0).CanSet(),
"③ 导出嵌入:整层可换、子字段可写,无任何封条")
mustPanic("① stickyRO.h.X.SetInt(1)", func() {
vs.Field(0).Field(0).SetInt(1)
})
ve.Field(0).Field(0).SetInt(42)
h := ve.Interface().(embedRO)
ok(h.X == 42, "② embedRO.hidden.X.SetInt(42) 真的写进去了(flagEmbedRO 不下传到导出子字段)")
}
// ===== 验证三:StructTag------惰性解析的 KV 协议 =====
func demo3Tag() {
fmt.Println("\n===== 验证三:StructTag------Get/Lookup 与四种形态 =====")
tt := reflect.TypeOf(TagDemo{})
for i := 0; i < tt.NumField(); i++ {
f := tt.Field(i)
val, found := f.Tag.Lookup("json")
fmt.Printf(" %s: tag原文=%-46q Lookup(json)=(%q, ok=%v)\n",
f.Name, string(f.Tag), val, found)
}
v, okA := tt.Field(0).Tag.Lookup("db")
ok(okA && v == "col_a", "多 key:A 的 db=\"col_a\"(惰性解析、每次重算不缓存)")
_, okC := tt.Field(2).Tag.Lookup("json")
ok(okC, "C=`json:\"\"`:ok=true、值为空------空值也算有 key")
_, okD := tt.Field(3).Tag.Lookup("json")
ok(!okD, "D=`json:badform`:格式错误静默跳过、ok=false(只有 vet 的 structtag 检查器会警告)")
_, okE := tt.Field(4).Tag.Lookup("json")
ok(!okE, "E 无 tag:ok=false ------ 而 Get 对 C/D/E 一律返回 \"\",分不清三种情况 → Lookup 优于 Get")
}
// ===== 验证四:嵌入与提升------NumField 不展开,查找靠 BFS =====
func demo4Promote() {
fmt.Println("\n===== 验证四:嵌入与提升------同深度冲突、外层遮蔽、深层路径 =====")
tl := reflect.TypeOf(Leaf{})
fmt.Printf(" Leaf.NumField=%d(只有 Mid;Grand/Deep 是提升字段,不算直接字段)\n", tl.NumField())
f, found := tl.FieldByName("Deep")
ok(found && fmt.Sprint(f.Index) == "[0 0 0]",
"FieldByName(\"Deep\") 沿嵌入链找到,Index=[0 0 0]------Index 是路径不是下标")
fmt.Printf(" VisibleFields(Leaf) 摊平(含中间层): ")
for _, vf := range reflect.VisibleFields(tl) {
fmt.Printf("%s%v ", vf.Name, vf.Index)
}
fmt.Println()
_, found2 := reflect.TypeOf(OnlyDup{}).FieldByName("Name")
ok(!found2, "OnlyDup.A1.Name 与 A2.Name 同深度冲突 → FieldByName 找不到(模糊即丢弃)")
fmt.Printf(" VisibleFields(OnlyDup): ")
var names []string
for _, vf := range reflect.VisibleFields(reflect.TypeOf(OnlyDup{})) {
fmt.Printf("%s%v ", vf.Name, vf.Index)
names = append(names, vf.Name)
}
fmt.Println()
has := func(n string) bool {
for _, x := range names {
if x == n {
return true
}
}
return false
}
ok(!has("Name") && has("UID") && has("Alias"),
"VisibleFields 只丢弃冲突的 Name;无冲突的提升字段 UID/Alias 照常返回(丢弃按"字段"算,不连坐整个嵌入)")
fs, found3 := reflect.TypeOf(Shadow{}).FieldByName("Name")
ok(found3 && fmt.Sprint(fs.Index) == "[2]",
"Shadow.Name 直接字段遮蔽两个提升的 Name(浅度赢,Index=[2])")
}
// ===== 验证五:FieldByIndex 过 nil 指针------panic 与 FieldByIndexErr =====
func demo5IndexNilPtr() {
fmt.Println("\n===== 验证五:指针嵌入、VisibleFields 摊平、nil 路径 =====")
pe := PtrEmbed{} // Address 为 nil
vpe := reflect.ValueOf(&pe).Elem()
fmt.Printf(" VisibleFields(PtrEmbed): ")
for _, vf := range reflect.VisibleFields(reflect.TypeOf(pe)) {
fmt.Printf("%s%v ", vf.Name, vf.Index)
}
fmt.Println("← 指针嵌入也摊平,City/Zip 的 Index 是两段路径 [0 0]/[0 1]")
mustPanic("nil 指针嵌入上 FieldByIndex([0 0])", func() {
vpe.FieldByIndex([]int{0, 0})
})
if _, err := vpe.FieldByIndexErr([]int{0, 0}); err != nil {
ok(true, fmt.Sprintf("FieldByIndexErr 返回 error 而非 panic:%v", err))
} else {
ok(false, "FieldByIndexErr 应当返回 error")
}
pe.Address = &Address{City: "BJ", Zip: 100000}
vpe.FieldByIndex([]int{0, 0}).SetString("SZ")
ok(pe.City == "SZ", "非 nil 时同一路径自动解引用且可 Set(Field 从不做逐层 nil 检查,检查是 FieldByIndexErr 的活)")
}
// ===== 验证六:迷你 json 字段映射器(文档 §7) =====
func demo6MiniJson() {
fmt.Println("\n===== 验证六:迷你 json------VisibleFields + tag + omitempty =====")
full := JItem{Base: Base{ID: 7, Tags: "go"}, Addr: Address{City: "BJ", Zip: 100000}, Score: 9.5, Secret: "s3cret", note: "n"}
empty := JItem{}
fmt.Printf(" full → %s\n", toJsonString(full))
fmt.Printf(" empty → %s\n", toJsonString(empty))
s := toJsonString(full)
ok(strings.Contains(s, `"id":"7"`) && strings.Contains(s, `"tags":"go"`),
"嵌入 Base 的 id/tags 经摊平被序列化(Index=[0 0]/[0 1] 喂给 FieldByIndex)")
ok(!strings.Contains(s, "s3cret") && !strings.Contains(s, "{7"),
"`json:\"-\"` 与未导出 note 都被跳过;嵌入结构体本身也不再整体输出")
ok(strings.Contains(toJsonString(empty), `"id":"0"`),
"empty:tags/score 为零值被 omitempty 省略;id 不带 omitempty 所以零值也保留")
}
// toJsonString 迷你 json 字段映射器:VisibleFields 摊平 → 读 tag → FieldByIndex 取值。
// 玩具只做字符串化,无转义/递归/类型分支(真实实现见 encoding/json,思路一致分支完备)。
func toJsonString(v any) string {
rv := reflect.ValueOf(v)
if rv.Kind() != reflect.Struct {
panic("need struct")
}
var pairs []string
for _, f := range reflect.VisibleFields(rv.Type()) {
if !f.IsExported() { // VisibleFields 会列出未导出字段,跳过是 json 的规则
continue
}
if f.Anonymous { // 嵌入结构体本身不整体输出(其提升字段已摊平)------encoding/json 同款
continue
}
tag := f.Tag.Get("json")
name := strings.Split(tag, ",")[0]
if name == "" {
name = f.Name
}
if name == "-" {
continue
}
fv := rv.FieldByIndex(f.Index)
if strings.Contains(tag, ",omitempty") && fv.IsZero() {
continue
}
pairs = append(pairs, fmt.Sprintf("%q:%q", name, fmt.Sprint(fv.Interface())))
}
return "{" + strings.Join(pairs, ",") + "}"
}