必须传入结构体指针并调用elem()获取可寻址值,仅导出字段可设值,需用canset()校验,按kind()选择对应set*方法,指针字段需elem()后再设值,struct tag用于动态映射字段名。

用 reflect.Value.Elem().FieldByName() 获取可寻址字段
结构体变量必须是地址才能修改字段值,否则 reflect.Value.FieldByName() 返回的是只读副本。常见错误是传入普通结构体变量,调用 SetString() 时 panic 报错:reflect.Value.SetString using unaddressable value。
正确做法是先用 reflect.ValueOf(&s) 获取指针的反射值,再调用 Elem() 解引用到结构体本身:
s := MyStruct{Name: "old"}
v := reflect.ValueOf(&s).Elem() // 必须这样
field := v.FieldByName("Name")
if field.CanSet() {
field.SetString("new")
}
- 仅导出(大写开头)字段可被
FieldByName()访问且可设置 -
CanSet()必须为true才能调用Set*方法,它依赖于值是否可寻址 + 是否导出 - 如果结构体嵌套了匿名字段,需用
Field(i)遍历并检查Type().Name()或Anonymous标志
修改非字符串类型字段要匹配 Set* 方法
反射不支持泛型 Set,每个基础类型有对应方法:给 int 字段赋值不能用 SetString(),否则 panic:reflect.Value.SetString of non-string type int。
需根据字段实际类型选择方法,并确保传入值类型一致:
立即学习“go语言免费学习笔记(深入)”;
field := v.FieldByName("Age")
switch field.Kind() {
case reflect.Int, reflect.Int64:
field.SetInt(25)
case reflect.Bool:
field.SetBool(true)
case reflect.Float64:
field.SetFloat(3.14)
case reflect.String:
field.SetString("hello")
}
-
Kind()返回底层类型分类(如reflect.Int64),Type()返回具体类型(如int或int64),设值时优先看Kind() - 对
int字段调用SetInt(25)是安全的;但若字段是int32,必须用SetInt32(),否则 panic - 结构体字段为指针(如
*string)时,需先field.Elem()再设值,且原字段不能为nil
通过 reflect.StructTag 读取 tag 并映射字段名
实际业务中常需按 JSON 或数据库列名动态更新字段,这时不能硬编码结构体字段名,得靠 struct tag 做映射。比如 json:"user_name" 对应结构体字段 UserName。
遍历所有字段,解析 tag 提取 key,构建 name → field 映射表:
v := reflect.ValueOf(&s).Elem()
t := reflect.TypeOf(s)
tagMap := make(map[string]reflect.Value)
for i := 0; i < t.NumField(); i++ {
field := t.Field(i)
jsonTag := field.Tag.Get("json")
if jsonTag == "" || jsonTag == "-" {
continue
}
key := strings.Split(jsonTag, ",")[0]
if field.Type.Kind() == reflect.Ptr {
tagMap[key] = v.Field(i).Elem()
} else {
tagMap[key] = v.Field(i)
}
}
// 使用:tagMap["user_name"].SetString("alice")
- struct tag 解析必须用
field.Tag.Get("json"),不是field.Tag["json"] - tag 中带
omitempty不影响读取,但-表示忽略该字段 - 若字段是嵌入结构体且带 tag,需递归处理,否则
NumField()不会包含其内部字段
性能和安全边界:别在热路径频繁用反射改字段
反射操作比直接字段访问慢 10–100 倍,且绕过编译期类型检查。生产环境高频场景(如 HTTP 请求绑定、数据库扫描)建议预生成 setter 函数,用 reflect.MakeFunc 编译一次复用。
- 每次
reflect.ValueOf()和FieldByName()都有分配开销,尤其在循环中应避免重复调用 - 字段名拼写错误或类型不匹配只在运行时报 panic,测试覆盖率要覆盖所有可能字段组合
- 如果只是做「字段复制」或「浅层映射」,优先考虑代码生成(如
easyjson、go:generate)而非运行时反射
真正难的不是怎么改字段,而是判断该不该用反射——多数时候,结构体字段名已知、数量固定,硬编码访问更稳更快。反射该是兜底方案,不是默认姿势。










