Go结构体赋值是字段级值复制:值类型完整拷贝,引用类型仅复制头信息或地址,底层数据共享;深拷贝需手动实现、json序列化或用copier库,但须规避资源句柄和不安全字段。

结构体赋值默认是值拷贝,不是深拷贝也不是浅拷贝
Go 语言里没有“深拷贝”或“浅拷贝”的语言级定义,= 赋值只是字段级值复制:值类型字段(int、string、不含引用字段的 struct)完整复制;引用类型字段([]T、map[K]V、*T、chan T、func)只复制头信息或地址,底层数据仍共享。
常见错误现象:改 b.Tags[0] 后 a.Tags[0] 也变;*b.Ptr = 100 导致 *a.Ptr 变成 100;delete(b.Info, "k") 同时从 a.Info 消失。
- 这不是 bug,是 Go 的语义设计——所有赋值、传参、返回都如此
- 所谓“浅拷贝”是开发者对引用字段共享行为的描述,不是语言术语
- 所谓“深拷贝”必须显式实现,不存在一键触发的语法或内置函数
json.Marshal + json.Unmarshal 是最常用但有坑的深拷贝方式
序列化再反序列化能绕过引用共享,适合大多数业务场景,但要注意它隐含的限制:
-
json不支持chan、func、unsafe.Pointer,遇到会 panic - 未导出字段(小写首字母)不会被序列化,拷贝后为零值
-
time.Time、struct{...}等自定义类型需实现MarshalJSON/UnmarshalJSON - 浮点数精度可能丢失(如
float64经 JSON 编码后变成float64,但中间经过字符串转换) - 性能开销明显,频繁调用时比手动拷贝慢 5–10 倍
示例:
var b Person _ = json.Unmarshal(json.MustMarshal(a), &b)
手动深拷贝必须逐字段处理引用类型
手动实现可控性强、无依赖、性能好,但只适合字段明确、嵌套不深的结构体。关键操作如下:
Go 配置库,使用 spf13/viper — 分层优先级(flag > env >file > KV > default),提供 BindPFlag/BindPFlags、SetEnvPrefix + SetEnvKeyReplace 等功能。
立即学习“go语言免费学习笔记(深入)”;
-
[]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是引用类型(如*int),要解引用再赋值 -
*T:判空后解引用再取地址,if src != nil { dst = &(*src) };若T本身含引用字段,需进一步深拷贝其内部 - 不处理
chan、func、未导出字段——它们本就不能安全复制,手动实现天然规避了这些问题
copier 库能简化重复劳动,但不解决语义问题
github.com/jinzhu/copier 是目前最轻量、使用最广的第三方深拷贝库,它基于反射自动处理字段映射和引用类型遍历。
- 优点:一行代码完成拷贝,支持 struct → struct、slice → slice、map → struct 等多种组合
- 缺点:无法处理循环引用(会栈溢出),不支持
chan和func,性能比手动实现低约 30% - 更关键的是:它不会帮你决定“哪些字段该深拷贝”,比如一个
sync.Mutex字段,copier 会尝试复制(失败),而你本意可能是忽略或重置它
示例:
copier.Copy(&b, &a)
真正容易被忽略的,不是怎么选工具,而是拷贝前先问一句:这个结构体里哪些字段是“状态”,哪些是“资源句柄”?http.Client、sql.DB、sync.RWMutex 这类字段复制后往往失效甚至引发 panic,它们不该被深拷贝,而应被重置或跳过。

















