传结构体指针仅在结构体≥24字节或含隐式堆分配字段时提速;小结构体值传递更快,指针反而因逃逸导致GC压力上升;方法接收者应统一用指针以避免方法集不兼容。

什么时候传结构体指针真能提速?
只有当结构体“大”到拷贝开销明显时,用 *User 替代 User 才有意义。所谓“大”,不是主观感觉,而是看字段总大小是否超过 24 字节(3 个机器字长),或是否含 [1024]byte、[]byte、map[string]interface{} 这类隐式堆分配字段。
- 值传递
User{ID: 1, Name: "Alice", Settings: [1024]byte{}}:每次调用复制 1KB+,CPU 和内存带宽都吃紧 - 指针传递
*User:只传 8 字节地址,函数内访问字段需一次解引用,但省下的拷贝远超这点开销 - 小结构体如
type Point {X, Y int}(16 字节):值传递更快——编译器可内联、寄存器优化,加星号反而引入 cache miss 和逃逸风险
为什么用了指针反而变慢?常见逃逸陷阱
加 * 不等于优化,它可能让原本在栈上的变量“逃逸”到堆上,触发 GC 压力上升、延迟增加。典型现象是压测时 allocs/op 突增、BenchmarkNsPerOp 不降反升。
-
return &User{...}:局部变量取地址后返回,强制堆分配 -
process(&Point{1, 2}):临时结构体取地址,无法栈分配 - 闭包中捕获大结构体并传指针:哪怕只读,也可能因引用关系被判定为逃逸
- 把小结构体赋给
interface{}(如传入fmt.Printf("%v", u)):触发接口装箱,常伴随指针逃逸
验证方式:运行 go build -gcflags="-m -m" main.go,盯住 “escapes to heap” 提示。
方法接收者该用值还是指针?统一比纠结更重要
只要结构体任一方法需要修改字段,就必须用指针接收者(func (u *User) SetEmail()),否则编译失败;而一旦用了指针接收者,其他所有方法也应保持一致——否则调用方无法用同一个实例同时调用所有方法(值类型和指针类型的方法集不兼容)。
立即学习“go语言免费学习笔记(深入)”;
- 结构体字段 ≤ 3 个且不含大数组/切片:值接收者(
func (u User) String())更易内联,适合只读高频场景 - 结构体含
map、slice或需修改状态:必须指针接收者,且建议全方法统一 - 对外暴露的 API 类型(如导出结构体):优先用指针接收者,避免使用者误以为“传值安全”而引发并发问题
sync.Pool + 指针:高频分配场景的标配组合
HTTP 请求处理、日志写入、序列化等场景中,频繁 new 大对象(如 *Buffer、*RequestCtx)是 GC 主要压力源。此时指针不是为了“传参快”,而是为了把对象放进 sync.Pool 复用。
- 定义池:
var userPool = sync.Pool{New: func() interface{} { return &User{} }} - 获取:
u := userPool.Get().(*User),用前重置字段(别依赖零值) - 归还:
userPool.Put(u),务必在函数末尾或错误路径上执行 - 注意:池中对象无生命周期保证,不能存任何需长期持有的引用(如闭包捕获外部变量)
真正难的是判断“多大才算大”、以及“是否真的逃逸了”——这两点没法靠经验猜,得靠 go test -bench=. 和 -gcflags="-m" 一起看。











