Go reflect 包:结构体反射 —Field、Tag 与 VisibleFields

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. 大小随类型任意------1 字节(bool)到任意大(结构体套结构体);

  2. 类型静态 ------编译期定死,操作也编译期定死(+、len()、.Name);

  3. 赋值即复制字节------之后彼此独立。

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

规则(读源码+文档总结):

  1. 返回所有"可被选择子访问"的字段,含所有嵌入层级的提升字段;

  2. 深度浅者优先;同深度的重名字段按提升规则不返回(模糊即丢弃);

  3. 被外层同名字段遮蔽的内层字段不返回;

  4. 返回的 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, ",") + "}"
}
相关推荐
liangbo71 小时前
03-JDK 10/11 局部变量推断与 LTS:var 到底该怎么用
java
EnCi Zheng2 小时前
PostgreSQL 实战指南:安装配置、数据类型、索引选型、MVCC 事务与高可用备份全解析
数据库·sql·postgresql·索引·1024程序员节
Percep_gan2 小时前
调用天翼云的对象存储ZOS,很详细
java·1024程序员节
若亦_2 小时前
数据结构>>优先级队列(堆)
java·数据结构
泡海椒2 小时前
JQuick-SQL REST API 数据接入:应用层调用接口后注册分析表
数据库·sql·1024程序员节
Wang's Blog2 小时前
Java 项目实战: 交友项目 - 查询圈子个人动态:查询我的动态需求分析
java·开发语言·交友
denlaku2 小时前
JDK 23 新特性详解
java
code2cat2 小时前
Java高级篇之Hibernate:让对象映射到表,理解持久化与事务
java·后端·hibernate·1024程序员节