Go结构体赋值=默认浅拷贝,值类型字段完整复制,引用类型仅复制地址;深拷贝需手动Clone或第三方库如ulule/deepcopier,json序列化有诸多限制。

Go 里结构体赋值 = 默认就是浅拷贝,不是“要实现才有的行为”,而是语言底层机制;深拷贝必须显式处理,没有内置函数,也不能靠 copy() 或 json.Marshal/Unmarshal 一劳永逸——它们各有硬伤,得看字段类型和业务语义选方案。
结构体赋值 = 就是浅拷贝,不用写函数
只要写 b := a,Go 就完成了浅拷贝:值类型字段(int、string、不含引用字段的嵌套 struct)完整复制;引用类型字段([]T、map[K]V、*T、chan、func)只复制 header 或地址,底层数据仍共享。
常见错误现象:b.Tags[0] = "x" 导致 a.Tags[0] 也被改;b.Info["k"] = v 同步到 a.Info;*b.Ptr = 100 直接影响 *a.Ptr。
不需要封装函数来“实现”浅拷贝,= 本身已足够。想绕过共享?那就得进下一步。
立即学习“go语言免费学习笔记(深入)”;
手动实现深拷贝时,必须逐字段处理引用类型
手写 Clone() 是最可控的方式,尤其当结构体含 http.Client、sync.RWMutex、未导出字段或 unsafe.Pointer 时,序列化方案会直接 panic 或忽略。
关键点:
-
[]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含引用字段,继续深拷其内部 - nil map / nil slice 不会自动初始化,漏掉
make就 panic
示例片段:
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
func (c Config) Clone() Config {
dst := c
if c.Tags != nil {
dst.Tags = make(map[string]bool, len(c.Tags))
for k, v := range c.Tags {
dst.Tags[k] = v
}
}
return dst
}
用 json.Marshal/Unmarshal 快速“伪深拷贝”,但限制极多
这是零依赖、上手最快的方案,本质是序列化再反序列化,不是内存级深拷贝。
适用场景:纯 DTO、配置结构体,字段全可导出、不含不可序列化类型。
硬限制:
- 所有字段必须首字母大写(可导出),否则
json包直接跳过 - 遇到
func、chan、sync.Mutex、unsafe.Pointer会 panic -
time.Time变成字符串,nil []int变成空切片[],语义丢失 - 性能差,比手写
Clone()慢 10–100 倍,别用于高频路径
示例:
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
}
生产环境推荐 ulule/deepcopier,编译期生成、零反射开销
目前最稳妥的第三方方案:安装后通过注解控制字段行为,编译时生成专用拷贝代码,无运行时反射、类型安全、性能接近手写。
使用前提:
- 源结构体字段必须导出(首字母大写)
- 目标结构体字段需可写(也得导出)
- 支持
deepcopier:"field:ID"映射、deepcopier:"-"忽略、嵌套结构体和指针 - 对
nil map、nil slice自动初始化,不用手动make
安装与基本用法:
go get github.com/ulule/deepcopier
import "github.com/ulule/deepcopier"
type User struct {
Name string
Tags map[string]bool `deepcopier:"optional"`
}
u1 := User{Name: "a", Tags: map[string]bool{"x": true}}
u2 := User{}
deepcopier.Copy(&u1).To(&u2) // u2.Tags 已自动初始化并填充
真正难的从来不是“怎么拷”,而是“哪些字段该深、哪些该浅、哪些该忽略”——比如日志上下文里的 context.Context 字段,深拷没意义;又比如带锁的 sync.RWMutex,直接复制地址反而引发竞态。语义必须由你定义,工具只是执行者。

















