Golang反射遍历结构体所有字段的方法

必须传入结构体指针并调用.Elem()获取可寻址的reflect.Value,遍历前检查Kind为Struct且IsValid,字段名取自Type,值取自Value,未导出字段需用CanInterface()判断是否可访问。

reflect.ValueOf 获取结构体值并遍历字段

直接对结构体变量调用 reflect.ValueOf 得到的是一个 reflect.Value,但必须确保它可寻址(addressable)才能获取字段值。传入指针是最稳妥的做法,否则 v.Field(i) 可能 panic。

常见错误:传入非指针结构体变量,导致 v.Kind() == reflect.Structv.CanAddr() == false,后续调用 v.Field(i) 会报 panic: reflect: call of reflect.Value.Field on struct Value

  • 始终传入结构体指针:reflect.ValueOf(&myStruct)
  • 再用 .Elem() 获取实际结构体值:v := reflect.ValueOf(&s).Elem()
  • 遍历前检查 v.Kind() == reflect.Structv.IsValid()

reflect.TypeOfreflect.ValueOf 配合获取字段名与值

字段名来自类型信息(reflect.Type),字段值来自值信息(reflect.Value),二者需同步索引。注意:匿名字段(嵌入结构体)的字段也会被列出,且导出性不影响遍历——但不可导出字段的 Value 无法读取(v.Field(i).CanInterface() == false)。

示例中若字段未导出(如 age int),v.Field(i).Interface() 会 panic;应先判断 v.Field(i).CanInterface() 或用 v.Field(i).CanSet() 辅助判断可访问性。

type User struct {
    Name string
    age  int // 小写,不可导出
}
u := User{Name: "Alice", age: 30}
v := reflect.ValueOf(&u).Elem()
t := reflect.TypeOf(u)

for i := 0; i < v.NumField(); i++ {
    field := t.Field(i)
    value := v.Field(i)
    if value.CanInterface() {
        println(field.Name, ":", value.Interface())
    } else {
        println(field.Name, ": (unexported, cannot read)")
    }
}

处理嵌套结构体和指针字段时的注意事项

反射遍历时,v.Field(i) 返回的 reflect.Value 可能是结构体、指针、切片等任意类型。若字段是指针(如 *Time),需先判断 value.Kind() == reflect.Ptr 并调用 value.Elem() 才能继续展开;若为 nil 指针,value.Elem() 会 panic。

  • 安全展开指针字段:先检查 !value.IsNil() 再调用 value.Elem()
  • 嵌套结构体字段可递归处理,但需限制深度防止无限循环(如循环引用)
  • reflect.ValueInterface() 方法在字段不可导出时失效,此时只能用 String() 或跳过

性能与替代方案:什么情况下不该用反射遍历

反射在 Go 中开销显著:每次 reflect.ValueOfField()Interface() 都涉及运行时类型检查和内存分配。简单结构体字段枚举(如 JSON 序列化、日志打印)建议手写方法或用代码生成工具(如 stringereasyjson)。

只有当字段数量动态变化、结构体类型未知(如 ORM 映射、通用校验器)时,才值得引入反射。另外,go:generate + structtag 等静态分析方式比运行时反射更安全、更快。

真正难处理的不是遍历本身,而是统一处理导出/非导出、nil 指针、接口值、递归嵌套这四类边界情况——漏掉任一,线上就可能 panic。