反射常用于解析标签、生成配置和实现序列化工具。过去遍历字段,需要先调用 NumField,再按索引获取。

Go 1.26 为 reflect.Typereflect.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.FieldsValue.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,结果只有 IDAudit,不会产生 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 适合同时处理字段描述和值。它们把常见的索引循环包装成更自然的迭代接口,但不会绕过反射原有的类型、访问和性能边界。