Go反射调用方法时结构体参数默认浅拷贝,值类型字段完整复制,引用类型字段仅复制头信息或指针,底层数据共享;深拷贝需手动实现,因未导出字段、循环引用、chan/func等无法安全处理。

反射调用方法时结构体参数默认是浅拷贝
Go 的 reflect.Value.Call 传参不改变语言底层的值传递规则:传入的结构体参数仍是字段级值复制。对值类型字段(int、string、纯值 struct)是完整副本;对引用类型字段([]T、map[K]V、*T)只复制头信息或指针,底层数据仍共享。
这意味着:即使你用反射调用一个修改 Tags 切片的方法,只要原始结构体的 Tags 和反射传入的副本指向同一底层数组,修改就会透出到原结构体。
- 反射不会自动触发深拷贝——它只是“换了一种方式做赋值”
-
reflect.ValueOf(&s).MethodByName("Modify").Call([]reflect.Value{...})中,如果传的是reflect.ValueOf(s)(非指针),那仍是结构体值拷贝 - 若想避免副作用,必须在反射调用前手动深拷贝结构体,或统一用指针接收者 + 传指针给
Call
用反射实现深拷贝时,未导出字段和循环引用是硬伤
常见反射深拷贝工具(如 github.com/mohae/deepcopy 或手写递归 reflect.Copy)会在这些地方失败:
- 未导出字段(首字母小写)无法通过
reflect.Value.Field访问,直接跳过,导致拷贝不完整 -
chan、func、unsafe.Pointer类型字段无法安全复制,多数库 panic 或静默忽略 - 结构体含自引用(如树节点有
*Node字段指向自己)时,递归会无限深入,除非显式维护 visited map - 接口类型(
interface{})内部实际值类型未知,反射需先.Elem()再判断,容易 panic
所以别指望反射“一键深拷贝”,它适合字段全导出、无 channel/func、无循环的简单场景。
立即学习“go语言免费学习笔记(深入)”;
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
json.Marshal/Unmarshal 不是万能深拷贝,注意类型丢失和性能
很多人用 json.Marshal + json.Unmarshal 实现深拷贝,但它有明确边界:
- 不支持未导出字段:
json默认只序列化导出字段,私有字段直接丢弃 - 类型退化:
time.Time变成字符串,int64可能被反解为float64(JSON 无整数类型),map[interface{}]interface{}无法 round-trip - 零值覆盖:目标结构体中已初始化的字段(如
sync.Mutex)在 Unmarshal 后被重置为零值,可能引发 panic - 性能差:涉及内存分配、字符串解析、类型转换,比手动 copy 慢 10–100 倍,不适合高频调用
它只适合“临时隔离简单数据”的场景,比如把配置结构体传给子 goroutine 做只读快照。
真正可控的深拷贝必须按业务语义手动处理
没有银弹。最可靠的方式是放弃通用性,针对具体结构体写专用拷贝函数:
- 对
[]T:用make([]T, len(src))+copy(dst, src);若T是引用类型(如[]map[string]int),需递归深拷每个元素 - 对
map[K]V:先make(map[K]V),再遍历赋值;若V是指针或 slice,要判断并深拷其内容 - 对
*T:检查是否为 nil,非 nil 时解引用再取地址(&(*src)),若T本身含引用字段,继续展开 - 对嵌套结构体:逐字段判断类型,值类型直赋,引用类型走对应分支——这正是编译器无法替你做的部分
复杂点在于:不是所有引用都该深拷。比如某个 *Config 字段本就是全局单例,深拷反而破坏设计。所以“深”或“浅”,本质是业务契约,不是技术开关。

















