在 Go 中,结构体 (struct) 默认是按值传递和返回的。这意味着在调用函数或返回时会复制整个结构体。对于小结构体来说,这是透明的,但对于大的结构体,问题就显得很关键。
最初,Go 的目标是高效地处理少量分配。但当结构体使用诸多字段和嵌套对象时,无意识地复制大数据的风险就出现了。这类操作的性能可能会受到影响,有时差异仅在性能分析或 GC 的痛苦中显现。
如果结构体的大小很大,每次调用函数、返回或赋值时的复制都会造成开销。这导致了:
对于大的结构体,推荐传递和返回指向结构体的指针 (*T),而不是结构体本身。这降低了开销并确保只处理一个数据实例。
代码示例:
package main import "fmt" type Large struct { Data [1024]int } // 按值传递(对于大对象不正确) func ValueProcess(l Large) { l.Data[0] = 123 // 只会修改副本 } // 按指针传递 func PointerProcess(l *Large) { l.Data[0] = 456 // 会修改原始值 } func main() { a := Large{} ValueProcess(a) fmt.Println("After ValueProcess:", a.Data[0]) // 0 PointerProcess(&a) fmt.Println("After PointerProcess:", a.Data[0]) // 456 }
关键特点:
1. 可以将结构体的局部变量指针从函数返回吗?
可以。Go 保证这样的指针是有效的,自动将返回指针所指向的值移动到堆中(转移到堆中)。
func NewLarge() *Large { l := Large{} return &l }
2. 如果将结构体按值传入函数并在内部更改字段,原始值会改变吗?
不会:只会更改副本,函数外的原始值保持不变。
3. 结构体总是使用指针吗?
不是。对于小型(有几个字段)的结构体,按值传递是安全的,且常常更合适(不可变值语义),节省分配并减少 GC 的负担。
在日志服务中,每个事件都是一个大结构体,并且按值从函数返回 — 每次修改都会复制整个结构体。
优点:
缺点:
改为通过指针传递和返回结构体,使用签名类型 func(l *Large) 和 func() *Large 来修改数据。
优点:
缺点: