结构体赋值默认是浅拷贝,仅对引用字段(如切片、map、指针)共享底层数据,值类型字段则完全复制;深拷贝需手动逐字段处理引用类型或借助序列化/第三方库,但各有局限和风险。

结构体赋值默认是浅拷贝,但只对引用字段生效
Go 语言里没有“深拷贝”这个语言特性,struct 赋值(如 copy := original)本质是逐字段复制:值类型字段(int、string、不含引用字段的嵌套 struct)完整复制内存;引用类型字段([]T、map[K]V、*T、chan T)只复制头信息或指针地址。这意味着修改 copy.Slice[0] 或 copy.Map["key"],original 对应位置也会变。
常见错误现象:在函数中接收 struct 参数后修改其切片字段,调用方发现原始数据被污染;或者并发写入时因共享底层数组 panic。
- 值类型字段天然隔离,无需额外处理
- 引用字段才是“浅”的源头,也是 bug 高发区
- 即使整个
struct是值类型,只要含引用字段,就不是完全独立副本
手动深拷贝必须逐字段处理引用类型
想真正隔离数据,就得自己写逻辑——不能只 copy := original,得对每个引用字段做递归复制。比如:
-
[]T:先make([]T, len(src)),再copy(dst, src);若T本身含引用(如[]map[string]int),需遍历每个元素再深拷贝 -
map[K]V:先make(map[K]V),再for k, v := range src { dst[k] = v };若V是*T或map,要递归处理 -
*T:判空后解引用再取地址,如if src != nil { dst = &(*src) };若T含引用字段,还得继续展开
这种写法性能好、可控性强,但字段一多就容易漏掉某个引用字段,尤其嵌套层级深时。未导出字段、func、chan、unsafe.Pointer 在手动实现时天然跳过,反而省去反射的兼容性麻烦。
用 json.Marshal/json.Unmarshal 做深拷贝有隐含限制
序列化再反序列化是最常用的“偷懒”方案,但实际踩坑不少:
- 字段必须是导出的(首字母大写),否则
json包直接忽略 - 不支持
chan、func、unsafe.Pointer,遇到会 panic - 浮点数精度可能丢失(
float64→ JSON number →float64可能四舍五入) - 时间类型(
time.Time)默认转成字符串,再解析回来可能时区/格式错乱 - 性能开销比手动复制大得多,尤其大数据量时明显
它适合快速验证逻辑或原型阶段,但上线服务里频繁调用,得掂量 CPU 和 GC 压力。
第三方库如 github.com/mohae/deepcopy 并非银弹
这类库用反射自动处理嵌套,省去手写逻辑,但问题也很实在:
- 反射本身慢,且无法绕过 Go 的导出规则——未导出字段照样不可见
- 对循环引用(A.field → B,B.field → A)会无限递归,直接栈溢出
- 不支持自定义类型的方法(比如实现了
MarshalJSON的 struct),仍按默认反射逻辑走 - 依赖版本更新滞后,某些新语法(如泛型约束下的复杂类型)可能解析失败
真正需要深拷贝的场景,往往意味着业务语义明确——哪些字段必须隔离、哪些可以共享。这时候硬套通用库,反而不如针对关键字段写几行确定性代码来得稳。
深拷贝从来不是“有没有”的问题,而是“要不要”和“怎么控”的问题。引用字段的边界在哪、并发安全要求多高、性能容忍度多少,这些才是决定方案的关键。随便选个 deepcopy 库或一行 json 就跑,最容易在压测或线上流量突增时暴露出来。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











