
修改 struct 字段值前必须确保可寻址
反射修改值失败,十有八九是因为 reflect.Value 不可寻址(CanAddr() == false),比如直接对函数参数、字面量或 map 中的 value 调用 reflect.ValueOf()。这时调用 Set* 方法会 panic:reflect.Value.SetXxx called on non-settable value。
真正能改的,只有指针指向的底层值,或者从可寻址变量(如局部变量、切片元素、结构体字段)反射出来的值。
- 正确做法:传入指针,再用
Elem()获取目标值 ——v := reflect.ValueOf(&myStruct).Elem() - 结构体字段要可导出(首字母大写),否则
FieldByName()返回零值且CanSet() == false - 切片/数组元素可直接通过索引获取并修改:
sliceVal.Index(0).SetInt(42),前提是 slice 本身可寻址 - map 中的 value 永远不可寻址,想改只能重新赋值整个键值对:
mapVal.SetMapIndex(keyVal, newVal)
Set* 方法类型必须严格匹配
反射设值不是“自动类型转换”,SetInt() 只接受 int64,SetString() 只接受 string,哪怕源值是 int 或 int32,也得先显式转成对应类型,否则 panic:reflect: cannot convert int to int64。
常见场景是处理 JSON 解析后未明确类型的 interface{} 值,比如从 json.Unmarshal 得到的 map[string]interface{} 里取数字,默认是 float64,不能直接塞给 SetInt()。
立即学习“go语言免费学习笔记(深入)”;
- 检查类型再转换:
if v.Kind() == reflect.Float64 { target.SetInt(int64(v.Float())) } - struct 字段类型为
*int时,不能用SetInt(),得先Addr().Interface()转成指针,再赋值 -
Set()方法更灵活,但要求源reflect.Value类型兼容(同 Kind 且可赋值),仍不支持跨整数宽度隐式转换
嵌套结构体和 interface{} 的递归赋值容易崩
当需要把一个 map[string]interface{} 动态填充进 struct,或把 struct 导出为类似 JSON 的嵌套结构时,手动递归调用 FieldByName() 和 Set() 很容易漏掉指针解引用、接口断言失败、或 nil 指针 panic。
关键不是“能不能做”,而是“在哪一层该取 Elem()”——比如字段是 *User,你得先确认它非 nil,再 FieldByName("User").Elem().Set(...);如果是 interface{} 字段,还得用 Set(reflect.ValueOf(realValue)),不能直接 SetInterface()。
- 每次访问前加
v.IsValid() && v.CanInterface()防止空值崩溃 - 对
interface{}字段,先v.Interface()再类型断言,比直接v.Elem()更安全 - 嵌套 struct 字段为 nil 指针时,
Elem()会 panic,应先if v.IsNil() { v.Set(reflect.New(v.Type().Elem())) }
性能敏感场景下避免在热路径反复反射
反射操作比直接赋值慢 10–100 倍,尤其 FieldByName() 这种靠字符串查找的,内部是线性遍历字段列表。如果在 HTTP handler 或循环中频繁用它填 struct,CPU 会明显升高。
真正要用反射的,应该是初始化阶段(如配置绑定、ORM 映射注册),而不是每次请求都重来一遍。
- 提前缓存
reflect.Type和字段索引:fields := make(map[string]int); for i, f := range t.NumField() { fields[f.Name] = i } - 用
reflect.StructField.Offset+unsafe直接内存写(极少数场景,需极度谨慎) - 考虑 code generation(如
go:generate+stringer风格模板),把反射逻辑编译期展开
最常被忽略的一点:reflect.Value 本身带额外字段和状态,反复创建它比复用已有值开销更大;能传 reflect.Value 就别传 interface{} 再现场 ValueOf。










