示例:
package main import ( "fmt" "reflect" ) func main() { var a int64 = 10 fmt.Println(reflect.TypeOf(a)) fmt.Println(reflect.ValueOf(a)) }ValueOf方法:
unpackEface方法:
代码其实很简单.但是好多疑问的点.下面是我自己总结出来的疑惑的点.以及为什么.
Value结构体:
type Value struct { typ_ *abi.Type // 类型元数据 ptr unsafe.Pointer // 数据指针(含义随 flagIndir 变化) flag // 嵌入的 flag uintptr:状态位域 }flag 是一个uintptr 位域,把"这个值的元状态"全部打包在一个机器字里(value.go:71-83):
| 位 | 名字 | 含义 |
|---|---|---|
| bit 0–4 | flagKindMask | Kind 枚举(27 种 Kind 恰好装进 5 位) |
| bit 5 | flagStickyRO | 来自未导出的非嵌入字段 → 只读 |
| bit 6 | flagEmbedRO | 来自未导出的嵌入字段 → 只读 |
| bit 7 | flagIndir | ptr是"指向数据的指针";未置位则数据本身就内联在ptr里 |
| bit 8 | flagAddr | CanAddr()==true(隐含 flagIndir 且 ptr 非空) |
| bit 9 | flagMethod | 这是一个 method value(绑定接收者的方法值) |
| bit 10+ | — | method value 的方法编号(方法表下标) |
所以f := flag(t.Kind())做的事非常朴素:把 Kind 枚举数值放进 flag 的低 5 位(flag就是uintptr的类型别名,flag(x)只是类型转换,不是函数调用)。
为什么要问两遍 Kind(typ_ 里不是有吗)
value.go:50的注释:"The lowest five bits give the Kind of the value, mirroring typ.Kind()"。两个原因:
快:
v.Kind()(value.go:1747)只需v.flag.kind(),即一次加载 + 一次掩码,不追指针去解引用typ_再读Kind_。flag 和 ptr 在同一 cacheline 友好的小结构里。method value 的 Kind 与 typ_ 脱钩:
v.Method(i)返回的 Value 中,typ_/ptr描述的是接收者 r,而 flag 的 Kind 位强制写成Func,方法编号放在 bit 10+(见value.go:64-68的注释)。此时flag.kind()才是权威答案,typ_.Kind()反而不是。
flag 支配了 Value 的所有"能力判断"
后面几乎所有操作第一步都是查 flag 位:
| 操作 | 检查 | 位置 |
|---|---|---|
CanAddr() | flag & flagAddr != 0 | value.go:342 |
CanSet() | flag&(flagAddr\|flagRO) == flagAddr | value.go:351 |
SetInt等写入 | flagRO != 0 \|\| flagAddr == 0→ panic | value.go:243 |
v.pointer() | flagIndir != 0→ 多解一层*(*unsafe.Pointer)(v.ptr) | value.go:108 |
Call | flagMethod != 0→ 用高位方法号查methodReceiver | value.go:393 |
Interface()(packEface) | 三分支按IfaceIndir/flagIndir组合搬运数据 | value.go:118 |
对ValueOf产出的 Value 来说:flag 里只有 Kind 位和可能的 flagIndir。没有flagAddr(所以CanAddr/CanSet都是 false,任何Set*直接 panic),也没有 RO 位(RO 只在Field访问未导出字段时打上)。
if t.IfaceIndir() { f |= flagIndir }的意义
先看IfaceIndir本体(internal/abi/type.go:206):
// IfaceIndir reports whether t is stored indirectly in an interface value. func (t *Type) IfaceIndir() bool { return t.Kind_&KindDirectIface == 0 }翻译:"这个类型的值放进 interface 时,数据字存的是指针(间接),而不是值本身(直接)"。
Go 的 interface 二元组是(类型指针, 数据字),数据字只有一个机器字:
指针形状类型(Pointer、Map、Chan、Func、UnsafePointer 五种,编译器给它们的
Kind_打上KindDirectIface位):值本身就是单个指针,直接塞进数据字,直接存储。其余所有类型(int、string、struct、slice……):值要么不止一个字、根本不是指针,装不进数据字 → 必须先把值拷贝到一块稳定内存(编译器生成
runtime.convT调用做堆分配,也就是reflectStudy/asm/box.go里Box(x Foo) any干的事),数据字存指向拷贝的指针,间接存储。
于是这段代码的意义就清楚了:
if t.IfaceIndir() { // 类型是间接存进 interface 的 f |= flagIndir // 那么 e.Data 是"指向数据的指针", }flagIndir 定义了ptr字段的解读方式(value.go:55:"flagIndir: val holds a pointer to the data"):
| flagIndir | ptr 里是什么 | 取真实指针 |
|---|---|---|
| 置位 | 指向数据的地址 | *(*unsafe.Pointer)(v.ptr),多解一层 |
| 未置位 | 数据(指针形状值)本身 | v.ptr直接就是 |
两个对照例子(配合 asm 学习):
type Foo struct{ A int; B string } v1 := reflect.ValueOf(Foo{1, "x"}) // Foo 非指针形状 → 装箱时 convT 堆拷贝,e.Data 指向拷贝 // v1.ptr = 指向堆上 Foo 的指针,flag = Struct | flagIndir v2 := reflect.ValueOf((*Foo)(nil)) // *Foo 指针形状 → 值本身就是指针,e.Data 就是它 // v2.ptr = nil(指针值本身),flag = Pointer,无 flagIndir两个补充要点:
不变量(
value.go:58注释):"If ifaceIndir(typ), code can assume that flagIndir is set"。整包代码都依赖它——例如反方向的packEface(Value.Interface(),value.go:118)用IfaceIndir与flagIndir的三分支对齐搬运数据。flagIndir 与 Kind 正交:连 Ptr 类型的 Value 也可能带 flagIndir——比如
Call的栈上返回值(value.go:617:fl := flagIndir | flag(tv.Kind())),ptr 指向栈帧里存放指针的那个字。所以 flagIndir 不能从 Kind 推出来,必须在构造时显式记录;unpackEface里就是用IfaceIndir()提供这个信息。
abi.Type的Kind_值具体存了哪些信息
Kind_是一个字节(type Kind uint8,internal/abi/type.go:49),被拆成两段用:
| 位段 | 内容 | 说明 |
|---|---|---|
| 低 5 位 | Kind 枚举 | Invalid=0, Bool=1, Int=2, … UnsafePointer=26共 27 种(type.go:51-79),掩码KindMask = (1<<5)-1 |
| bit 5 | KindDirectIface | 指针形状标志:值本身就是单个指针字,直接存进 interface 数据字;仅对 Pointer/Map/Chan/Func/UnsafePointer 五种 Kind 由编译器置位 |
读取时必须先掩掉标志位(type.go:197):
func (t *Type) Kind() Kind { return t.Kind_ & KindMask }三个容易混淆的点:
Kind()返回的只是低 5 位;Q2 里的IfaceIndir()读的是同一个字节的 bit 5(t.Kind_&KindDirectIface == 0),IsDirectIface()读 bit 5 的反面。一个字节两种信息。reflect.Kind与abi.Kind是两个独立类型但数值一一对应。reflect/type.go:271自己定义了type Kind uint及同样的 27 个常量(注释还写明"known to ../internal/abi/type.go"),所以flag(t.Kind())、Kind(f & flagKindMask)这类跨包转换是零成本的纯 reinterpret。别和
TFlag混了。TFlag是abi.Type里另一个独立的标志字节(type.go:89-128),存的是"附加元数据"的线索:TFlagUncommon(有方法表)、TFlagExtraStar(名字多带个*)、TFlagNamed(命名类型)、TFlagRegularMemory(可当整块内存比较/哈希)、TFlagGCMaskOnDemand(GC 位掩码延迟生成)。口诀:Kind_的 bit5 说的是"值长什么形状",TFlag说的是"类型旁边还挂了什么"。
顺带一提,源码里这两个 Kind 标志位的常量上方留着官方吐槽(type.go:82):
// TODO (khr, drchase) why aren't these in TFlag? Investigate, fix if possible. KindDirectIface Kind = 1 << 5通俗易懂版:
技术版看累了就来看这节。核心图景一句话:
interface 是一张只有两格的快递单(类型格 + 数据格);
Value是你拿到手后的"取件凭证 + 说明书";flag 就是那张说明书。
A. flag:Value 的"说明书"
Value就三样东西:
type Value struct { typ_ *abi.Type // 这是个什么类型 ptr unsafe.Pointer // 数据在哪(或数据本身) flag // 说明书:一串开关 + 编号挤在一个整数里 }flag 是个普通整数,被掰成好几格,像行李箱上的标签贴纸,一张写好几件事:
| 格子 | 写的是什么(大白话) |
|---|---|
| 低 5 位 | "箱子里装的是第几类东西"(Kind 编号 0~26) |
| 第 5 位 | "封条:来自小写字段,别动"(StickyRO) |
| 第 6 位 | "封条:来自小写嵌入字段,别动"(EmbedRO) |
| 第 7 位 | "注意:ptr 是储物柜号牌,不是东西本身"(Indir) |
| 第 8 位 | "允许开箱改东西"(Addr) |
| 第 9 位 | "这不是普通值,是个绑定了接收者的方法"(Method) |
| 高位 | 方法在方法表里的编号 |
所以f := flag(t.Kind())不是函数调用,就是类型转换:把 Kind 编号(比如 Struct 是 25)抄进说明书第一栏。
typ_ 里已经有类型了,为什么还要抄一份编号进 flag?
快:以后问
v.Kind()直接从 flag 拿,一步掩码完事,不用顺着 typ_ 指针跑去另一块内存读。有个场景只能靠 flag 说话:
v.Method(i)拿到的"方法值"里,typ_/ptr 装的其实是接收者(那个对象),真正的身份"我是个函数"和方法编号没地方放,只能写进 flag。
说明书决定你能干什么:CanAddr()看第 8 位,CanSet()看第 8 位且封条位没贴,Set系列发现没章直接 panic。ValueOf开出来的说明书上只有 Kind 和(可能的)Indir,没有"允许开箱"章——这就是reflect.ValueOf(x)得到的值永远不能 Set 的原因。
B.IfaceIndir() → flagIndir:给"号牌"盖章
背景:任何值塞进any时,Go 只给两个格子:一格放类型指针,一格放数据。数据格只有一个指针大小(8 字节)。于是值有两种命运:
天生就是地址的值(指针、map、chan、func、unsafe.Pointer):本身就是 8 字节地址,直接放进格子,完事。
其他所有值(int、string、struct、slice……):要么不止 8 字节,要么根本不是地址,塞不进格子→ Go 先把值复制一份到堆上(
convT干的事,box.go汇编里看的就是它),格子里放这份拷贝的地址。
IfaceIndir()问的就是:"这个类型装箱走的哪条路?"返回 true = 走第二条路(格子里放的是地址,间接的)。
f |= flagIndir就是在盖章:"ptr 是储物柜号牌,不是东西本身"。两个例子对照:
v1 := reflect.ValueOf(Foo{1, "x"}) // Foo 塞不进格子 → 堆上复制一份 → v1.ptr = 拷贝的地址(号牌) // 说明书:Struct + Indir 章 v2 := reflect.ValueOf((*Foo)(nil)) // *Foo 本身就是地址,直接放格子 → v2.ptr = 指针值本身(就是东西) // 说明书:Pointer,没有 Indir 章以后 reflect 内部每次用数据都先看这个章:有章就顺着地址再走一步取真东西(*(*unsafe.Pointer)(v.ptr)),没章就ptr 里已经是真东西,直接用。章盖错了就是空指针崩溃或拿到垃圾数据,所以必须在造 Value 的那一刻记准。
C.Kind_:一个字节掰两半用
bit: 7 6 5 4 3 2 1 0 ┌───┬───┬───┬───────────────┐ map 的 Kind_: │ 0 │ 0 │ 1 │ 10101 │ = 53 └───┴───┴───┴───────────────┘ │ └── 编号 21 = Map(低 5 位) └── 指针形状开关:1 = 直接放格子,不走堆拷贝
低 5 位 = 编号:全 Go 只有 27 种基础种类(Invalid=0 … UnsafePointer=26),编号最多 26,5 个位(可表示 0~31)正好够。
第 5 位 = 开关:编译期写死——"这个类型的值是不是天生一个字、能直接放进 interface 数据格"。只有指针/map/chan/func/unsafe.Pointer 五种是 1。
读法也不同:t.Kind()先& 0x1f把开关遮掉只取编号;IfaceIndir()只看开关位。前文 Q2 和 Q3 其实是同一块内存的两个视角——IfaceIndir()读的就是这个字节的 bit 5。
防混淆:TFlag是隔壁另一张标签,写的是"这个类型旁边挂没挂附属资料"(方法表、名字、GC 信息……),跟"值长什么形状"无关。口诀:Kind_ 的 bit 5 说"值是什么形状",TFlag 说"类型旁边挂了什么"。
D. 三问串成一句话
ValueOf收到 any → 撕开 interface 拿到(类型 t,数据 e.Data)→ 看 t 的Kind_字节:低 5 位当编号抄进说明书,bit 5 决定数据格里放的是"地址"还是"值",是地址就给说明书盖 Indir 章→{t, ptr, flag}三件套打包还给你。