在Golang开发中,结构体是承载复杂数据的常用类型,当结构体包含指针类型字段或者嵌套了其他包含指针的结构体时,直接使用赋值操作得到的只是浅拷贝结果,拷贝对象和原始对象会共享指针指向的内存空间,修改拷贝对象的数据会同步影响原始对象,这在很多业务场景下会引发不符合预期的问题,因此需要掌握结构体深拷贝的实现方法,尤其是处理指针嵌套场景的技巧。

Golang浅拷贝与深拷贝的区别
首先需要明确两种拷贝的核心差异:
- 浅拷贝:只复制对象本身,对象内部的指针字段不会复制指向的内存,拷贝后的对象和原对象共享指针指向的数据。
- 深拷贝:不仅复制对象本身,还会递归复制对象内部所有指针指向的内存数据,拷贝后的对象和原对象完全独立,修改互不影响。
基础结构体的浅拷贝问题示例
先看一个包含指针嵌套的结构体浅拷贝场景:
package main
import "fmt"
// 定义内层结构体
type Inner struct {
Value int
}
// 定义外层结构体,包含指针类型的嵌套字段
type Outer struct {
Name string
Inner *Inner
}
func main() {
// 初始化原始对象
origin := Outer{
Name: "origin",
Inner: &Inner{
Value: 10,
},
}
// 直接赋值,触发浅拷贝
copyObj := origin
// 修改拷贝对象的指针字段值
copyObj.Inner.Value = 20
// 打印两个对象的内层值
fmt.Println("原始对象Inner.Value:", origin.Inner.Value) // 输出20,原始对象被修改
fmt.Println("拷贝对象Inner.Value:", copyObj.Inner.Value) // 输出20
}
上述代码中,copyObj := origin只复制了Outer的结构体本身,Inner指针字段两个对象指向同一个内存地址,因此修改拷贝对象的Inner.Value会影响原始对象。
指针嵌套场景下的深拷贝实现方法
方法一:手动逐字段复制
对于结构固定的场景,可以手动为每个字段赋值,遇到指针字段就新建对应的内存空间并复制内容:
package main
import "fmt"
type Inner struct {
Value int
}
type Outer struct {
Name string
Inner *Inner
}
// 手动实现深拷贝函数
func DeepCopyOuter(origin Outer) Outer {
// 复制外层结构体的非指针字段
copyObj := Outer{
Name: origin.Name,
}
// 处理指针嵌套字段,新建内存并复制内容
if origin.Inner != nil {
copyObj.Inner = &Inner{
Value: origin.Inner.Value,
}
}
return copyObj
}
func main() {
origin := Outer{
Name: "origin",
Inner: &Inner{
Value: 10,
},
}
// 调用深拷贝函数
copyObj := DeepCopyOuter(origin)
// 修改拷贝对象的指针字段值
copyObj.Inner.Value = 20
// 打印结果
fmt.Println("原始对象Inner.Value:", origin.Inner.Value) // 输出10,原始对象未被修改
fmt.Println("拷贝对象Inner.Value:", copyObj.Inner.Value) // 输出20
}
这种方法的优点是逻辑清晰,没有额外依赖,缺点是需要为每个结构体单独编写拷贝函数,结构体字段变更时需要同步修改拷贝逻辑,适合结构稳定、结构体数量少的场景。
方法二:使用encoding/gob序列化反序列化
Golang标准库的encoding/gob包可以将对象序列化为字节流再反序列化为新对象,这个过程会自动处理指针嵌套,实现深拷贝:
package main
import (
"bytes"
"encoding/gob"
"fmt"
)
type Inner struct {
Value int
}
type Outer struct {
Name string
Inner *Inner
}
// 使用gob实现深拷贝
func DeepCopyByGob(origin Outer) (Outer, error) {
var buf bytes.Buffer
// 序列化原始对象
enc := gob.NewEncoder(&buf)
err := enc.Encode(origin)
if err != nil {
return Outer{}, err
}
// 反序列化为新对象
var copyObj Outer
dec := gob.NewDecoder(&buf)
err = dec.Decode(©Obj)
if err != nil {
return Outer{}, err
}
return copyObj, nil
}
func main() {
origin := Outer{
Name: "origin",
Inner: &Inner{
Value: 10,
},
}
copyObj, err := DeepCopyByGob(origin)
if err != nil {
fmt.Println("深拷贝失败:", err)
return
}
// 修改拷贝对象的指针字段值
copyObj.Inner.Value = 20
// 打印结果
fmt.Println("原始对象Inner.Value:", origin.Inner.Value) // 输出10
fmt.Println("拷贝对象Inner.Value:", copyObj.Inner.Value) // 输出20
}
这种方法的优点是不需要手动编写每个字段的拷贝逻辑,适合结构体结构复杂、字段频繁变更的场景,缺点是序列化和反序列化会有一定的性能开销,不适合高频调用的场景。
方法三:使用第三方反射库
如果项目中需要频繁处理各种结构体的深拷贝,也可以使用基于反射的第三方库,比如github.com/jinzhu/copier,通过反射递归处理所有字段,自动处理指针嵌套:
package main
import (
"fmt"
"github.com/jinzhu/copier"
)
type Inner struct {
Value int
}
type Outer struct {
Name string
Inner *Inner
}
func main() {
origin := Outer{
Name: "origin",
Inner: &Inner{
Value: 10,
},
}
var copyObj Outer
// 调用copier的Copy方法实现深拷贝
err := copier.Copy(©Obj, &origin)
if err != nil {
fmt.Println("深拷贝失败:", err)
return
}
// 修改拷贝对象的指针字段值
copyObj.Inner.Value = 20
// 打印结果
fmt.Println("原始对象Inner.Value:", origin.Inner.Value) // 输出10
fmt.Println("拷贝对象Inner.Value:", copyObj.Inner.Value) // 输出20
}
这种方法的优点是兼容性好,支持各种复杂结构体,使用简单,缺点是需要引入第三方依赖,反射操作也会有一定的性能损耗。
不同深拷贝方法的选型建议
可以根据实际场景选择合适的方法:
| 实现方法 | 适用场景 | 优点 | 缺点 |
|---|---|---|---|
| 手动逐字段复制 | 结构体数量少、结构稳定、高频调用 | 无依赖、性能高 | 维护成本高,字段变更需要同步修改 |
| gob序列化反序列化 | 结构体复杂、字段频繁变更、调用频率不高 | 标准库实现,无需额外依赖,逻辑通用 | 性能开销较大 |
| 第三方反射库 | 项目中大量不同结构体的深拷贝需求 | 使用简单,兼容性好 | 引入第三方依赖,反射有性能损耗 |
注意事项
- 如果结构体包含循环指针引用,递归深拷贝会导致无限循环,需要提前处理循环引用的问题。
- 如果结构体包含不可序列化的字段(比如channel、函数类型),使用gob序列化或者反射库的方式会失败,需要选择手动拷贝的方式处理这些特殊字段。
- 深拷贝会复制所有指针指向的数据,如果指针指向的数据很大,会产生较大的内存开销,需要评估内存使用情况。