必须传入结构体指针并调用.Elem()获取可寻址的reflect.Value,遍历前检查Kind为Struct且IsValid,字段名取自Type,值取自Value,未导出字段需用CanInterface()判断是否可访问。
reflect.ValueOf 获取结构体值并遍历字段直接对结构体变量调用 reflect.ValueOf 得到的是一个 reflect.Value,但必须确保它可寻址(addressable)才能获取字段值。传入指针是最稳妥的做法,否则 v.Field(i) 可能 panic。
常见错误:传入非指针结构体变量,导致 v.Kind() == reflect.Struct 但 v.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.Struct 和 v.IsValid()
reflect.TypeOf 和 reflect.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.Value 的 Interface() 方法在字段不可导出时失效,此时只能用 String() 或跳过反射在 Go 中开销显著:每次 reflect.ValueOf、Field()、Interface() 都涉及运行时类型检查和内存分配。简单结构体字段枚举(如 JSON 序列化、日志打印)建议手写方法或用代码生成工具(如 stringer、easyjson)。
只有当字段数量动态变化、结构体类型未知(如 ORM 映射、通用校验器)时,才值得引入反射。另外,go:generate + structtag 等静态分析方式比运行时反射更安全、更快。
真正难处理的不是遍历本身,而是统一处理导出/非导出、nil 指针、接口值、递归嵌套这四类边界情况——漏掉任一,线上就可能 panic。