反射常用于解析标签、生成配置和实现序列化工具。过去遍历字段,需要先调用 NumField,再按索引获取。
Go 1.26 为 reflect.Type 和 reflect.Value 增加了 Fields 方法。它没有改变反射规则,但让字段遍历更直接。
过去如何遍历字段
传统写法需要手动维护索引:
t := reflect.TypeOf(user)
v := reflect.ValueOf(user)
for i := 0; i < t.NumField(); i++ {
field := t.Field(i)
value := v.Field(i)
fmt.Println(field.Name, value)
}
类型和值依靠同一个 i 关联,代码较为机械。
Type.Fields 遍历字段定义
Type.Fields 返回 iter.Seq[reflect.StructField],每次产生一个字段定义:
t := reflect.TypeOf(User{})
for field := range t.Fields() {
name := field.Tag.Get("json")
fmt.Println(field.Name, name)
}
StructField 包含名称、类型、标签和索引路径等信息,适合 ORM 映射、标签校验和文档生成。
遍历顺序与依次调用 Field(0) 到 Field(NumField()-1) 相同,也支持直接 break。
Value.Fields 同时获取类型和值
Value.Fields 返回 iter.Seq2[reflect.StructField, reflect.Value],一次迭代即可获得字段定义和值:
v := reflect.ValueOf(user)
for field, value := range v.Fields() {
tag := field.Tag.Get("json")
fmt.Println(tag, value)
}
如果传入指针,需要先调用 Elem。对指针、切片等非结构体类型直接调用 Fields 会 panic,通用工具应检查 Kind 和 nil 指针。
修改字段时的边界
Value.Fields 与 Value.Field(i) 遵循相同规则。原始结构体可寻址且字段可设置时,才能修改字段:
for _, value := range v.Fields() {
if value.Kind() == reflect.String && value.CanSet() {
value.SetString(strings.TrimSpace(value.String()))
}
}
修改前必须检查 CanSet。未导出字段通常不能设置,调用 Interface 前还应检查 CanInterface,否则可能 panic。
嵌入字段不会自动展开
Fields 只遍历直接字段。假设 Order 包含 ID 和匿名嵌入的 Audit,结果只有 ID 与 Audit,不会产生 Audit.CreatedAt。需要展开时,仍要根据 field.Anonymous 和字段类型自行递归。
新写法是否更快
Go 1.26 的实现仍按索引遍历 NumField,再调用 Field(i) 产生结果。新 API 改善的是表达方式,并不承诺性能提升。
高频路径仍应使用 Benchmark 测量,并尽量在初始化阶段缓存字段信息。
使用时记住这些边界
Type.Fields返回字段定义Value.Fields同时返回字段定义和值- 两者只按声明顺序遍历直接字段
- 非结构体类型调用会 panic,指针需要先解引用
- 修改前检查
CanSet,读取接口值前检查CanInterface - 需要 Go 1.26 工具链;使用迭代器
range时,模块语言版本至少为 Go 1.23,建议直接设置为go 1.26.0
写在最后
Type.Fields 适合分析结构体定义,Value.Fields 适合同时处理字段描述和值。它们把常见的索引循环包装成更自然的迭代接口,但不会绕过反射原有的类型、访问和性能边界。