1. StructField:字段的全套元数据:
reflect/type.go:1000:
Go
type StructField struct {
Name string // 字段名
PkgPath string // 未导出字段的包路径;导出字段为空
Type Type // 字段类型
Tag StructTag // tag 原文
Offset uintptr // 相对结构体起始的字节偏移
Index []int // 本字段在其 StructType.Fields 中的下标路径
Anonymous bool // 是否为嵌入字段(无字段名的字段)
}
注意区分两条获取路径:
-
Type.Field(i)→ 返回 StructField(元数据),静态信息,不涉及值; -
Value.Field(i)→ 返回 Value(值句柄),可继续读写。
Index 是"路径"而不只是"下标":对顶层字段是 [i];通过 FieldByIndex([]int{0,1}) 逐层下钻时,它是从根到字段的完整路径(FieldByIndexErr 会用它报告是哪一层出错)。
值和句柄:
A. 值:内存里那堆字节本身:
Go
x := 42 // x 就是那 8 个字节
u := User{...} // u 就是那 24 个字节(16 字符串头 + 8)
y := x // 值拷贝:把字节复制一份,y 和 x 从此无关
值的三个特征:
-
大小随类型任意------1 字节(bool)到任意大(结构体套结构体);
-
类型静态 ------编译期定死,操作也编译期定死(
+、len()、.Name); -
赋值即复制字节------之后彼此独立。
B. 句柄:固定三件套的"指过去"凭证:
reflect.Value 就是一个句柄(reflect/value.go:39):
Go
type Value struct {
typ_ *abi.Type // 它是什么(档案指针)
ptr unsafe.Pointer // 它在哪(号牌:指向数据,或内联数据本身------01b Q2 的 flagIndir 决定)
flag // 说明书:Kind、只读封条、flagAddr、方法位(01b Q1 的位域表)
}
它不是值,是"指着一个值的三件套" 。句柄是个大家族:文件描述符(一个 int 指向打开的文件)、窗口句柄 HWND、*sql.DB------共同点都是小、固定、不透明、货在别处 。unsafe.Sizeof(v) 恒为 24(amd64 上 3 个字),与被指的值多大无关。
Go
x := 42
v := reflect.ValueOf(&x).Elem() // v = 句柄 {int档案, &x, Int|Indir|Addr}
v.SetInt(99) // 顺句柄改到本体 → x==99
C. 拷贝语义:复制字节 vs 复印单子:
Go
v2 := v // 拷贝句柄 ≠ 拷贝值!只是复印提货单
v2.SetInt(7) // x==7 ------ 两张单子指向同一个货
对照 A 节的 y := x:值拷贝是复制字节(从此独立),句柄拷贝是复制指向(仍指同一份)。这与坑清单第 5 条互为镜像:
-
ValueOf(x)在装箱边界 发生一次值拷贝(01b 附6 装箱①:convT 把 x 复制上堆)------所以ValueOf(x)的句柄永远够不着原变量; -
想够得着:
ValueOf(&x).Elem()(指针进 + Elem 开 = flagAddr); -
之后句柄之间传递(传参、赋值)全是浅拷贝,共享同一个本体。
D. 对照表:
| 值 | 句柄(reflect.Value) | |
|---|---|---|
| 是什么 | 数据字节本身 | {档案指针, 数据指针, flag} 三件套 |
| 大小 | 1B ~ 任意 | 恒定 3 个字(24B) |
| 拷贝语义 | 深拷贝字节,从此独立 | 浅拷贝,仍指同一份数据 |
| 类型 | 各有各的静态类型 | 任何值装进句柄都是同一个类型 |
| 操作 | 编译期定死(x+1、.Name) |
运行期查 flag 分派(v.Int()、v.Field(0)) |
| 能否写 | 变量本身可赋值 | 看 flag:flagAddr 有、RO 封条无 → CanSet() |
E. 为什么必须发明句柄:统一信封
Go 是静态类型语言,函数签名编译期定死,写不出 func f(x 任意类型且能读写)。句柄就是把"任意类型的值 + 能力说明"装进一个统一类型的信封 ------json.Marshal、ORM、校验器全靠它。
any 是同思想的穷亲戚:也能装任意值(两格:类型+数据,01b 附6),但装进去就只剩"读类型",丢了定位与读写能力;reflect.Value 是带操作能力的强化信封 ------多出 flag(能力位),且句柄之间传递不再发生装箱(ptr 直传,没有 convT)。
顺带一层同构:类型元数据自己也是"被句柄指着的大对象"------*rtype 装进 reflect.Type 时指针形状直存(01b 附7),Value 指着数据、Type 接口指着档案,两边用的是同一套"两格/三件套"哲学。
F. 回到 Field:档案 vs 句柄(structdemo 验证一的语境):
Go
t.Field(0) // StructField:纯静态档案(Name/Tag/Offset),不含"数据在哪"------与任何对象无关
v.Field(0) // Value:句柄,ptr = 结构体基址 + Offset(value.go:1267)------绑定"这个对象的这个字段"
问"这个字段叫什么、tag 是什么、偏移多少"→ 查档案(Type.Field);问"这个对象的 这个字段现在是多少、能不能改"→ 拿句柄(Value.Field)。档案全程序共享一份(rodata),句柄每个对象一张。
G. 大白话:提货单:
-
值 = 货:8 字节的 int、24 字节的 User,实实在在躺在变量里;赋值就是把货复印一份。
-
句柄 = 提货单 :
reflect.Value三件套------单子上写着货是什么类型(typ_)、几号柜(ptr)、以及一张说明书(flag)。 -
说明书上的章 :
flagIndir标注"柜号是储物柜还是货本身"(01b Q2);flagStickyRO/flagEmbedRO是封条(01b Q1);flagAddr是"允许开箱改货"章------没有它,Set*直接 panic。 -
复印单子 :
v2 := v只是多了张单子,货还是那一件------改 v2 就是改 x。 -
统一信封 :寄件处(函数参数)只收统一规格的信封,什么货都得先装信封------
any是只够看一眼的信封,Value是附了操作说明书的信封。
一句话收束:Type.Field 给的是档案复印件(全程序一份),Value.Field 给的是提货单(一单一货);读元数据问档案,读写数据凭单子。
2. Value.Field 源码逐行读(value.go:1267):
这是全包最值得精读的 20 行之一------权限位如何传播、地址如何计算,全在这:
Go
func (v Value) Field(i int) Value {
if v.kind() != Struct {
panic(&ValueError{"reflect.Value.Field", v.kind()})
}
tt := (*structType)(unsafe.Pointer(v.typ()))
if uint(i) >= uint(len(tt.Fields)) {
panic("reflect: Field index out of range")
}
field := &tt.Fields[i]
typ := field.Typ
// 权限位继承:保留父值的 RO/间接/可寻址位,但清掉 flagEmbedRO
fl := v.flag&(flagStickyRO|flagIndir|flagAddr) | flag(typ.Kind())
// 未导出字段强制只读:嵌入的打 flagEmbedRO,普通的打 flagStickyRO
if !field.Name.IsExported() {
if field.Embedded() {
fl |= flagEmbedRO
} else {
fl |= flagStickyRO
}
}
// 地址 = 结构体基址 + 字段偏移。
// flagIndir 未置位时 v.ptr 本身就是数据(内联小结构体),此时 offset 必为 0
ptr := add(v.ptr, field.Offset, "same as non-reflect &v.field")
return Value{typ, ptr, fl}
}
逐行拆解四个要点:
① 类型强转零拷贝 :(*structType)(unsafe.Pointer(v.typ())) 把公共头视图转成 struct 专属视图,Fields 数组紧跟在头部之后。
② 权限位继承而非独立计算 :子字段的 RO 状态 = 父值 RO 状态 ∪ 自身是否导出。经典后果------Field 链式下钻时,只要任何一层是未导出字段,整条路径都只读:
Go
type Inner struct{ Secret int }
type Outer struct{ inner Inner } // 未导出字段
v := reflect.ValueOf(&Outer{}).Elem()
v.FieldByName("inner").FieldByName("Secret").SetInt(1) // panic: unexported
③ flagEmbedRO 与 flagStickyRO 的区别 (第一篇 3.3 埋的伏笔):v.Field(i) 只继承 flagStickyRO 不继承 flagEmbedRO。语义是:"嵌入字段带来的只读"止步于本层(Go 规范:通过未导出嵌入字段访问其导出字段是允许的),而"显式未导出字段"的只读向下粘住每一层(sticky 之名由此而来)。这是为了与 reflect.Value.Field 的安全模型对齐------嵌入字段的方法和导出成员可以被提升访问,但直接 Set 该嵌入字段整体不行。
④ 地址计算统一为一个加法 :无论多深的嵌套,Field 都不递归,FieldByIndex 只是循环调用 Field,每次 ptr + offset。reflect 从不做"逐层解引用检查 nil 指针"------FieldByIndex 遇到 nil 指针路径会 panic,FieldByIndexErr(1.15+)才返回 error。
3. StructTag:约定俗成的 KV 协议:
tag 是编译进元数据的字符串,标准格式(伪 EBNF):
Go
tag := [space] key ':' '"' value '"' { space key ':' '"' value '"' }
StructTag.Get 的实现要点(Get 在 type.go:1036,Lookup 在 type.go:1047):
Go
// 惯例格式:空格分隔的 key:"value" 对(vet 的 structtag 检查器会校验格式)
func (tag StructTag) Lookup(key string) (value string, ok bool)
-
解析是惰性且每次重算 的:没有缓存,每次
Get/Lookup都重新扫描字符串。高频路径(如 json 序列化)的库会自己解析一次后缓存。 -
格式错误的 tag 静默跳过(不报错),
Lookup的ok区分"无此 key"与"值为空"。 -
Get是Lookup的封装:无 key 时返回空串。
实战示例------标准库级 tag 读取器:
Go
func fieldTags(t reflect.Type) map[string]map[string]string {
out := map[string]map[string]string{}
for i := 0; i < t.NumField(); i++ {
f := t.Field(i)
m := map[string]string{}
for _, key := range []string{"json", "db", "validate"} {
if v, ok := f.Tag.Lookup(key); ok {
m[key] = v
}
}
out[f.Name] = m
}
return out
}
4. 嵌入字段与提升规则:
Go 语言的字段/方法提升在 reflect 里的投影:
-
直接字段:
Type.Field(i)只列出声明在该结构体上的字段,不展开嵌入; -
提升字段:需要
FieldByName沿嵌入链查找(BFS,同深度冲突则丢弃),或用VisibleFields一次性摊平。
Go
type Base struct{ ID int }
type Item struct {
Base
Name string
}
t := reflect.TypeOf(Item{})
t.NumField() // 2(Base、Name),提升的 ID 不算
t.Field(0).Anonymous // true(Base 是嵌入字段)
FieldByName("ID") 能找到 Base.ID,返回的 Index 是 [0, 0](路径)。
5. VisibleFields:一次摊平所有可见字段:
visiblefields.go(Go 1.17 加入):
Go
func VisibleFields(t Type) []StructField
规则(读源码+文档总结):
-
返回所有"可被选择子访问"的字段,含所有嵌入层级的提升字段;
-
深度浅者优先;同深度的重名字段按提升规则不返回(模糊即丢弃);
-
被外层同名字段遮蔽的内层字段不返回;
-
返回的
Index是完整路径,可直接喂给Value.FieldByIndex。
它是 encoding/json 等正确处理匿名字段的官方姿势,替代手写 BFS。
Go
for _, f := range reflect.VisibleFields(t) {
fmt.Println(f.Index, f.Name, f.Tag.Get("json"))
}
6. StructOf:运行时构造结构体(了解即可):
type.go:2179 的 StructOf(fields []StructField) Type 可以在运行时拼出新结构体,但限制很多(官方注释直说 "panics if passed unexported StructFields"):未导出字段直接 panic;嵌入字段的方法提升不支持(大量嵌入场景会 panic,Issue 15924 至今未解)。tag 倒是完整支持------newName(name, tag, ...) 把 tag原样编进字段名编码(type.go:2544)。主要给 DI/ORM 框架做动态模型用,日常业务代码几乎不该用。
7. 综合实战:迷你 json 字段映射器:
把本篇知识点串起来(可运行,建议动手敲):
Go
package main
import (
"fmt"
"reflect"
"strings"
)
type User struct {
ID int `json:"id"`
Name string `json:"name"`
Email string `json:"email,omitempty"`
}
func toJsonString(v any) string {
rv := reflect.ValueOf(v)
if rv.Kind() != reflect.Struct {
panic("need struct")
}
t := rv.Type()
var pairs []string
for _, f := range reflect.VisibleFields(t) {
tag := f.Tag.Get("json")
name := strings.Split(tag, ",")[0]
if name == "" {
name = f.Name
}
if name == "-" {
continue
}
omit := strings.Contains(tag, ",omitempty")
fv := rv.FieldByIndex(f.Index)
if omit && fv.IsZero() {
continue
}
pairs = append(pairs, fmt.Sprintf(`%q:%q`, name, fmt.Sprint(fv.Interface())))
}
return "{" + strings.Join(pairs, ",") + "}"
}
func main() {
fmt.Println(toJsonString(User{ID: 1, Name: "ann"}))
// {"id":"1","name":"ann"} ------ email 因 omitempty 被省略
}
8. 常见坑清单:
| 现象 | 原因 | 解法 |
|---|---|---|
Set* panic: unexported field |
flagStickyRO 粘住 | 无法绕过(设计如此);改用导出字段或提供 Setter |
FieldByName 找不到提升字段 |
名字冲突被丢弃,或跨包未导出嵌入 | 打日志看 VisibleFields 实际返回 |
| tag 读不到 | 格式错(缺引号/冒号两边空格不对) | 严格 key:"value",用 Lookup 检查 |
FieldByIndex panic |
路径上有 nil 指针 | 用 FieldByIndexErr |
| 修改不生效 | ValueOf 拿的是副本 |
传指针再 Elem() |
9. 本篇小结:
-
Type.Field给元数据,Value.Field给值句柄;Index是路径不是下标; -
Field的 20 行源码浓缩了权限位传播(sticky vs embed)与偏移地址计算; -
tag 是惰性解析的 KV 字符串,
Lookup优于Get(可区分空值); -
提升字段靠
FieldByName(BFS)或VisibleFields(摊平+遮蔽规则); -
想写值:指针进、
Elem()开、导出字段才能 Set。
示例:
Go
// 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, ",") + "}"
}