Go值类型传参是字段级值复制:基本字段完整拷贝,引用类型字段仅复制header(如切片data指针),底层数组等仍共享;大数组全量拷贝,开销真实存在。

Go 中值类型传参或赋值时会复制整个值,但“复制什么”取决于字段是否含引用类型——这不是深拷贝,也不是传统意义的浅拷贝,而是 Go 特有的字段级值复制行为。
值类型传参时到底拷贝了什么
Go 所有传参都是值传递,struct、array、string 等值类型传入函数时,编译器会在栈上分配新内存,逐字段复制。但注意:
- 基本字段(
int、string、嵌套小struct)被完整复制,修改不影响原变量 -
[]T、map[K]V、*T、chan这类字段只复制其 header(如切片的 data/len/cap),底层数组、哈希表、堆内存仍共享 - 大数组(如
[1024]byte)传参会拷贝全部 1KB 数据,不是指针,开销真实存在
典型现象:modify(p Person) 后 p.Name 不变,但 p.Tags[0] 改了原切片——因为 Tags 字段的 header 被复制,data 指针没变。
什么时候该用指针传参
不是“所有结构体都该传指针”,关键看大小和语义:
立即学习“go语言免费学习笔记(深入)”;
- 结构体总大小 ≤ 3 个机器字(通常 ≤ 24 字节,如 3 个
int64),值传递更高效,且避免nilpanic 风险 - 含大数组、长切片、大 map 或频繁被修改,必须用
*Person——只拷贝 8 字节地址,不碰底层数组 - 方法接收者也同理:
func (p *Person) Save()可改原数据;func (p Person) Clone()更适合构造不可变副本
误用指针的代价:多一次解引用、可能引入 nil panic、破坏不可变契约;误用值传递的代价:函数调用栈暴涨、GC 压力上升(若逃逸到堆)。
如何验证拷贝开销与逃逸行为
别猜,用 Go 自带工具看真实行为:
- 查结构体真实大小:
unsafe.Sizeof(MyStruct{}),比字段和还大?说明有内存对齐填充 - 看逃逸分析:
go build -gcflags="-m" main.go,若出现... escapes to heap,说明该值没在栈上分配,拷贝成本已转为 GC 开销 - 性能敏感路径加基准测试:
go test -bench=.对比process(s MyStruct)和process(s *MyStruct)的耗时与分配次数
常见陷阱:以为 for _, v := range slice 中的 v 是引用,其实它是每次迭代的值拷贝;大结构体遍历时应写 for i := range slice { process(&slice[i]) }。
需要真正独立副本时怎么办
Go 没有内置深拷贝,所谓“深”必须手动定义边界:
- 仅需隔离某几个字段(如切片、map):用
append([]T(nil), src...)或for k, v := range m { clone[k] = v } - 嵌套层级简单、字段全可导出:走
encoding/gob序列化再反序列化,最省心但慢、不支持func/chan/unsafe.Pointer - 高频、确定结构:手写
DeepCopy()方法,显式处理每个引用字段,性能最优也最可控
最易被忽略的一点:即使你做了“深拷贝”,如果结构体里存了 *os.File 或 net.Conn 这类资源句柄,复制出来的仍是同一底层 fd——这种“深”是语义无效的,得靠业务逻辑约束或封装成不可复制类型(如 sync.Mutex 已禁止拷贝)。










