Go中struct默认浅拷贝,赋值仅复制第一层,指针、map、slice等字段共享底层数据;深拷贝需用json序列化(限导出字段)、手动Clone方法(最可控)或第三方库(性能与安全权衡)。

Go 中 struct 默认是浅拷贝,直接赋值不等于深拷贝
Go 的所有值类型(包括 struct)在赋值时都会复制整个内存块,但这个“复制”只到第一层:如果结构体字段里有指针、map、slice、chan 或者包含这些类型的嵌套结构,那这些字段指向的底层数据**不会被递归复制**,而是被新旧变量共享。你改了副本里的 slice 元素,原结构体也会看到变化——这不是你想要的“深拷贝”。
常见错误现象:panic: assignment to entry in nil map 或修改副本后原结构体意外变更;使用场景多见于配置缓存、测试用例隔离、API 响应体克隆等需要数据完全独立的场合。
- 别依赖
reflect.Copy:它只支持同类型、同长度的 slice 复制,不是通用深拷贝方案 -
json.Marshal + json.Unmarshal最简单但有坑:字段必须是导出的(首字母大写),且会丢掉未导出字段、函数字段、chan、unsafe.Pointer等 - 自定义
Clone()方法最可控,适合字段稳定、性能敏感的结构体
用 json 序列化做深拷贝的实操要点
这是最快上手的方式,但必须清楚它的行为边界:
- 仅对导出字段生效 ——
type Config struct { Name string; password string }中的password字段会被忽略 - 非 JSON 可序列化类型会报错:
json: unsupported type: chan int、json: unsupported type: func() - 空接口
interface{}里的值如果含不可序列化类型,同样失败 - 性能开销明显:涉及内存分配、字符串转换、GC 压力,不适合高频调用(如每毫秒一次)
示例:
立即学习“go语言免费学习笔记(深入)”;
func DeepCopyByJSON(src interface{}) (interface{}, error) {
data, err := json.Marshal(src)
if err != nil {
return nil, err
}
var dst interface{}
err = json.Unmarshal(data, &dst)
return dst, err
}
为结构体实现 Clone() 方法的必要条件
当结构体字段明确、生命周期长、或含不可序列化类型时,手动克隆最可靠。关键是:每个字段都要按其语义决定怎么复制。
-
string、int、bool等基础类型:直接赋值即可 -
slice:用make([]T, len(src))+copy(),或append([]T(nil), src...) -
map:需遍历键值对,dst[key] = value—— 注意 value 是值类型才安全;若 value 是指针或 map,得递归处理 -
*T指针:要判断是否为 nil,再决定是否 new + copy;否则直接复制指针只是浅拷贝 - 嵌套结构体:调用其自身的
Clone()方法,形成链式委托
示例片段:
func (c Config) Clone() Config {
clone := c
if c.Data != nil {
clone.Data = &Data{Value: c.Data.Value}
}
clone.Tags = append([]string(nil), c.Tags...)
clone.Meta = make(map[string]string)
for k, v := range c.Meta {
clone.Meta[k] = v
}
return clone
}
第三方库如 copier 和 go-deepcopy 的取舍
它们用 reflect 实现通用深拷贝,省去手写逻辑,但代价是运行时开销和隐式行为。
-
github.com/jinzhu/copier:支持 tag 控制(如copier:"-")、字段名映射,但对循环引用无保护,可能栈溢出 -
github.com/mohae/deepcopy(已归档)或较新的go-deepcopy:更严谨,能检测循环引用并返回错误,但反射调用比手写慢 5–10 倍 - 兼容性风险:Go 版本升级后,某些 reflect 行为微调可能导致库内部 panic,比如 Go 1.22 对未导出字段的访问限制加强
- 无法静态分析:编译器看不到字段复制逻辑,字段增减后容易漏掉深拷贝,测试覆盖不到位就埋雷
建议只在原型阶段或结构体极不稳定(频繁增删字段)时用;上线服务优先选手写 Clone() 或 json(若满足约束)。
真正难的不是“怎么拷”,而是想清楚哪些字段必须深、哪些可以共享、哪些压根不该拷——比如一个 sync.Mutex 字段,深拷贝毫无意义,还可能引发死锁。










