目录
[1. 值类型](#1. 值类型)
[2. 引用类型](#2. 引用类型)
[1. 浅拷贝](#1. 浅拷贝)
[2. 深拷贝](#2. 深拷贝)
[1. 错误示范:浅拷贝的隐患](#1. 错误示范:浅拷贝的隐患)
[2. 正确实现:深拷贝的保障](#2. 正确实现:深拷贝的保障)
[五、& 和 *:操作符与类型的区别](#五、& 和 *:操作符与类型的区别)
[1. &:取地址操作符](#1. &:取地址操作符)
[2. *:指针类型标识](#2. *:指针类型标识)
[3. 简写写法:&entity.Cities{...}](#3. 简写写法:&entity.Cities{...})
在 Go 开发中,深浅拷贝是绕不开的基础问题,尤其在处理指针、切片等引用类型时,一旦理解不到位,很容易出现 "修改返回值污染原始数据" 的隐蔽 Bug。本文结合实际项目中 "城市缓存查询" 的场景,拆解深浅拷贝的核心逻辑,同时厘清 & 和 * 这两个易混淆符号的用法。
一、为什么go开发一定要关注深浅拷贝?
在本地生活类项目中,城市列表是高频访问的基础数据,我们通常会将全量城市数据加载到内存缓存中,提供给接口层调用。核心需求很明确:
- 缓存数据是全局共享的 "只读数据源",不允许外部修改;
- 接口返回城市数据时,要保证调用方的操作不会影响缓存本身。
先看一段简化后的核心代码(我的实习部门 c端项目中实际使用的逻辑):
package city
import (
"sync/atomic"
"your_project/entity"
)
// 全局城市缓存,基于atomic.Value保证并发安全
var allCityMemCache atomic.Value
// GetAllCity 获取全量城市数据
func GetAllCity() []*entity.Cities {
// 从原子缓存中读取原始数据
allCity := allCityMemCache.Load().([]*entity.Cities)
// 预分配返回切片容量,提升性能
retCities := make([]*entity.Cities, 0, len(allCity))
for _, city := range allCity {
// 逐字段创建新的城市对象
tmpCity := &entity.Cities{
ID: city.ID,
CountryCode: city.CountryCode,
ProvinceID: city.ProvinceID,
ProvinceName: city.ProvinceName,
Code: city.Code,
Name: city.Name,
// 省略其他字段...
}
retCities = append(retCities, tmpCity)
}
return retCities
}
// entity.Cities 结构体定义(简化版)
type Cities struct {
ID int // 城市ID
CountryCode string // 国家码
ProvinceID int // 省份ID
ProvinceName string // 省份名称
Code string // 城市编码
Name string // 城市名称
}
这段代码看似简单,但藏着深浅拷贝、指针操作的核心知识点。接下来我们一步步拆解。
二、值类型与引用类型
要理解深浅拷贝,首先要分清 Go 的两种数据类型,这是所有操作的基础:
1. 值类型
包括 int、string、bool、float、结构体(无嵌套指针)、数组等。这类类型的特点是:赋值时直接拷贝 "值本身",新旧变量互不影响。示例:
func demoValueType() {
type User struct {
Name string
Age int
}
u1 := User{Name: "张三", Age: 20}
u2 := u1 // 直接拷贝值
u2.Name = "李四"
// 输出:张三(u1不受u2修改影响)
println(u1.Name)
}
2. 引用类型
包括指针、切片、map、chan 等。这类类型的特点是:赋值时只拷贝 "指向数据的内存地址",新旧变量共享底层数据。示例:
func demoRefType() {
type User struct {
Name string
Age int
}
u1 := &User{Name: "张三", Age: 20}
u2 := u1 // 拷贝指针地址,共享底层数据
u2.Name = "李四"
// 输出:李四(u1被u2修改影响)
println(u1.Name)
}
三、深浅拷贝的定义
基于值类型和引用类型的特性,衍生出两种拷贝方式:
1. 浅拷贝
只拷贝 "表层数据",如果数据包含引用类型(指针、切片等),仅拷贝引用地址,新旧数据共享底层内存。通俗理解:复制了文件的 "快捷方式",修改快捷方式指向的文件,原文件也会变。
2. 深拷贝
递归拷贝所有层级的数据,包括引用类型指向的底层数据,新旧数据完全独立,互不影响。通俗理解:复制了文件本身,修改新文件,原文件毫无变化。
四、城市缓存场景中的深浅拷贝实践
回到开头的城市缓存代码,我们对比两种实现方式,看深浅拷贝的实际影响。
1. 错误示范:浅拷贝的隐患
如果图省事,直接返回缓存的指针切片,就是典型的浅拷贝:
// 错误:浅拷贝实现,会污染缓存
func BadGetAllCity() []*entity.Cities {
allCity := allCityMemCache.Load().([]*entity.Cities)
retCities := make([]*entity.Cities, 0, len(allCity))
for _, city := range allCity {
// 直接复用原始指针,仅拷贝地址
retCities = append(retCities, city)
}
return retCities
}
调用方一旦修改返回值,缓存的原始数据会被篡改:
// 调用浅拷贝版本的函数
cities := BadGetAllCity()
// 修改返回值中的城市名称
cities[0].Name = "假北京"
// 此时缓存中的原始数据也变成了"假北京",所有依赖该缓存的接口都会返回错误数据
2. 正确实现:深拷贝的保障
开头的 GetAllCity 函数是标准的深拷贝实现,核心逻辑是:遍历原始指针切片,为每个城市创建全新的结构体对象,并逐字段拷贝原始值,最终返回新对象的指针切片。
// 正确:深拷贝实现,新旧数据完全独立
func GetAllCity() []*entity.Cities {
allCity := allCityMemCache.Load().([]*entity.Cities)
retCities := make([]*entity.Cities, 0, len(allCity))
for _, city := range allCity {
// 创建全新的Cities对象,逐字段拷贝原始值
tmpCity := &entity.Cities{
ID: city.ID,
CountryCode: city.CountryCode,
ProvinceID: city.ProvinceID,
ProvinceName: city.ProvinceName,
Code: city.Code,
Name: city.Name,
}
retCities = append(retCities, tmpCity)
}
return retCities
}
此时调用方修改返回值,完全不会影响缓存:
cities := GetAllCity()
cities[0].Name = "假北京"
// 缓存中的原始数据依然是"北京",数据安全得到保障
五、& 和 *:操作符与类型的区别
在深拷贝实现中,&entity.Cities{...} 是核心写法,这里很容易混淆 & 和 * 的作用,我们拆开来讲:
1. &:取地址操作符
& 是一个操作符,不是类型,作用是 "获取某个值的内存地址"。示例:
// 1. 创建一个Cities类型的"值"
cityVal := entity.Cities{ID: 1, Name: "北京"}
// 2. 取该值的内存地址,得到指针
cityPtr := &cityVal
// 打印验证:cityPtr的类型是*entity.Cities,值是内存地址
fmt.Printf("类型:%T,内存地址:%p\n", cityPtr, cityPtr)
// 输出:类型:*entity.Cities,内存地址:0xc0000a6000
2. *:指针类型标识
* 用于定义指针类型 ,表示 "指向某个类型的指针",比如 *entity.Cities 表示 "指向 entity.Cities 结构体的指针"。Go 中不存在 &entity.Cities 这种类型,& 只是生成指针的 "动作",而非类型的一部分。
3. 简写写法:&entity.Cities{...}
项目中我们常用 &entity.Cities{...} 这种简写,等价于 "先创建值,再取地址":
// 简写写法(推荐)
tmpCity := &entity.Cities{ID: 1, Name: "北京"}
// 等价于分步写法
cityVal := entity.Cities{ID: 1, Name: "北京"}
tmpCity := &cityVal
这种简写的核心目的是:直接得到 *entity.Cities 类型的指针,匹配函数返回值 []*entity.Cities(指针切片)的类型要求。如果直接写 entity.Cities{...},得到的是值类型,无法添加到指针切片中,会触发编译错误。
六、总结
- 浅拷贝是 "复制地址",新旧数据共享内存,适合无修改场景,风险高;深拷贝是 "复制数据本身",新旧数据独立,适合只读缓存等场景;
- 对于
[]*T这类指针切片,深拷贝需要遍历 + 新建对象 + 逐字段赋值,避免共享底层数据; &是取地址操作符,*T是指针类型,&T{...}是创建值并取地址的简写,最终得到*T类型的指针;- 全局缓存场景中,深拷贝是保障数据安全的核心手段,即使牺牲少量性能,也能避免数据污染的致命问题。