Golang的reflect包提供了运行时反射能力,能够让程序在运行阶段获取变量的类型信息和值信息,其中获取结构体字段是反射非常常用的场景,比如处理未知结构体的序列化、动态校验结构体字段值等需求都需要用到相关能力。

reflect获取结构体字段的基础流程
要使用reflect获取结构体字段,首先需要把目标结构体转换为reflect.Type和reflect.Value类型,前者用于获取类型相关的元数据,后者用于获取和操作值。
1. 获取结构体的reflect.Type
可以通过reflect.TypeOf函数获取任意变量的类型信息,如果传入的是结构体实例,得到的就是对应的结构体类型。
package main
import (
"fmt"
"reflect"
)
// 定义测试用的结构体
type User struct {
Name string
Age int
// 小写开头的字段为私有字段,反射无法直接读取修改
score float64
}
func main() {
u := User{Name: "张三", Age: 20, score: 88.5}
// 获取结构体的reflect.Type
t := reflect.TypeOf(u)
// 判断是否为结构体类型
if t.Kind() != reflect.Struct {
fmt.Println("传入的不是结构体")
return
}
fmt.Println("结构体名称:", t.Name())
}
2. 遍历获取所有结构体字段信息
通过reflect.Type的NumField方法可以获取结构体的字段总数,再通过Field方法传入索引获取对应字段的reflect.StructField信息,里面包含字段名、类型、标签等元数据。
func main() {
u := User{Name: "张三", Age: 20, score: 88.5}
t := reflect.TypeOf(u)
// 遍历所有字段
for i := 0; i < t.NumField(); i++ {
field := t.Field(i)
fmt.Printf("字段索引:%d 字段名:%s 字段类型:%v 是否可导出:%vn",
i, field.Name, field.Type, field.IsExported())
}
}
上述代码执行后会输出三个字段的信息,其中score字段的IsExported结果为false,说明是私有字段,后续无法直接通过反射修改其值。
通过reflect读取结构体字段的值
要读取结构体字段的具体值,需要用到reflect.Value类型,通过reflect.ValueOf获取结构体的值对象,再通过Field方法传入索引或者字段名获取对应字段的值对象,最后通过对应的类型方法获取具体值。
func main() {
u := User{Name: "张三", Age: 20, score: 88.5}
v := reflect.ValueOf(u)
// 通过索引获取字段值
nameField := v.Field(0)
ageField := v.Field(1)
// 通过字段名获取字段值
nameFieldByName := v.FieldByName("Name")
// 获取具体的值,需要根据字段类型调用对应的方法
fmt.Println("Name字段值:", nameField.String())
fmt.Println("Age字段值:", ageField.Int())
fmt.Println("通过字段名获取的Name值:", nameFieldByName.String())
}
需要注意,如果字段是私有字段,调用Field或者FieldByName获取值对象时不会报错,但是调用对应类型方法获取值时会触发panic,所以读取字段值前最好先判断字段是否可导出。
通过reflect修改结构体字段的值
修改结构体字段值有两个前提条件:第一,结构体实例必须传入指针,否则获取到的是值拷贝,修改不会影响原实例;第二,要修改的字段必须是可导出字段,私有字段无法修改。
func main() {
u := User{Name: "张三", Age: 20, score: 88.5}
// 传入结构体指针获取value
v := reflect.ValueOf(&u).Elem()
// 检查value是否可设置
if !v.CanSet() {
fmt.Println("当前value不可设置")
return
}
// 获取Name字段的值对象
nameField := v.FieldByName("Name")
if nameField.IsValid() && nameField.CanSet() {
// 修改字段值,需要传入对应类型的值
nameField.SetString("李四")
}
// 修改Age字段
ageField := v.FieldByName("Age")
if ageField.IsValid() && ageField.CanSet() {
ageField.SetInt(25)
}
fmt.Printf("修改后的结构体:%+vn", u)
}
上述代码中通过Elem方法获取指针指向的实际值,再通过CanSet判断字段是否可修改,避免修改私有字段或者不可设置的字段触发panic。
获取结构体字段的标签信息
结构体字段的标签是Golang中很常用的特性,比如json序列化、表单校验等场景都会用到标签,通过反射也可以获取字段的标签内容。
// 带标签的结构体
type Product struct {
ID int `json:"id" validate:"required"`
Name string `json:"name" validate:"required,min=2"`
Price float64 `json:"price" validate:"required,gt=0"`
}
func main() {
p := Product{}
t := reflect.TypeOf(p)
for i := 0; i < t.NumField(); i++ {
field := t.Field(i)
// 获取json标签的值
jsonTag := field.Tag.Get("json")
// 获取validate标签的值
validateTag := field.Tag.Get("validate")
fmt.Printf("字段名:%s json标签:%s validate标签:%sn",
field.Name, jsonTag, validateTag)
}
}
通过reflect.StructField的Tag属性的Get方法,传入标签的键名就能获取到对应的标签值,如果标签不存在会返回空字符串。
注意事项总结
- 反射操作结构体字段时,私有字段无法直接读取具体值也无法修改,只能获取其元数据信息。
- 修改结构体字段值必须传入结构体指针,并且通过
Elem获取实际值对象,否则修改无效。 - 操作字段前建议通过
IsValid和CanSet方法判断字段是否存在、是否可修改,避免程序panic。 - 反射操作相比直接操作会有一定的性能损耗,非必要场景不建议过度使用反射。