小结构体(≤16字节)优先传值,大结构体(>64字节)或需修改原值时传指针;切片/map/channel本身是引用类型,无需额外加星号;接收者类型取决于是否修改状态,避免混用;逃逸分析(-gcflags="-m -l")是判断依据。

什么时候该用指针,什么时候不该用
Go 函数参数默认值传递,但“值”的大小和可复制性决定性能开销。小结构体(比如 struct{ x, y int })直接传值比传指针更快,因为避免了额外的内存寻址和可能的逃逸分析开销;而大结构体(如含 []byte、map 或上百字段的 struct)传指针更稳——不是怕慢,是怕编译器把变量分配到堆上,引发 GC 压力。
常见错误现象:go tool compile -gcflags="-m" main.go 显示 “moved to heap” 却没意识到是自己无意识传了大值导致的。
- 优先传值:基础类型(
int、string)、小结构体(字段总大小 ≤ 2×指针宽度,通常 ≤ 16 字节) - 必须传指针:需要修改原值、结构体含不可复制字段(如
sync.Mutex)、或明确知道它很大(> 64 字节且频繁调用) - 接口类型(
io.Reader等)本身已是引用语义,无需再取地址传&r
接收者用值还是指针?看是否要改状态
方法接收者类型不是风格选择,而是语义契约。用值接收者意味着“我不动你”,用指针意味着“我有权改你”。混淆会导致静默错误或 panic。
使用场景:如果类型实现了带指针接收者的方法(比如 (*bytes.Buffer).Write),却用值变量调用,会触发隐式取地址——但前提是该值是可寻址的。临时值(如 bytes.Buffer{}.)会编译失败:cannot call pointer method on bytes.Buffer literal。
立即学习“go语言免费学习笔记(深入)”;
- 值接收者:只读操作、小类型、避免意外修改(如
func (v Point) DistanceTo(o Point) float64) - 指针接收者:修改字段、同步原语(
sync.Mutex必须指针)、统一实现接口(只要有一个方法用了指针接收者,所有方法最好都用) - 别混合:同一个类型别一会值一会指针接收者,容易让调用方踩
cannot call pointer method的坑
切片、map、channel 本身已是指针包装,别多此一举
[]int、map[string]int、chan struct{} 这些类型在运行时底层都是结构体,内含指针。传它们本身已经是轻量级的“引用传递”,再套一层 *[]int 不仅没收益,反而增加解引用开销、破坏可读性、还可能引入 nil panic。
典型错误:写 func process(data *[]string),以为能“修改原切片头”,其实切片扩容后原变量仍指向旧底层数组——真正要重分配只能靠返回新切片或用指针接收者封装。
- 传
[]T足够:读写元素、追加(只要不扩容或扩容后不依赖原变量) - 需要扩容并让调用方看到新底层数组?返回新切片:
func extend(s []int) []int,而不是传*[]int -
map和chan同理:直接传,别加*;它们的零值(nil map)本身就能安全判空和赋值
逃逸分析是你的第一道检查线
要不要用指针,不能靠猜。Go 编译器会根据变量生命周期决定是否逃逸到堆。值传递有时反而触发逃逸(比如局部变量被闭包捕获),而合理用指针反而抑制逃逸(比如把大对象地址传入函数,避免复制后又堆分配)。
关键动作:用 go build -gcflags="-m -l" main.go 看每行的逃逸决策。“leaking param”、“moved to heap” 是信号,“no escape” 才是理想状态。
-
-l关闭内联,让分析更真实(否则内联可能掩盖逃逸) - 关注函数参数和返回值是否逃逸,而不是局部变量——后者影响小得多
- 别迷信“指针一定快”:一个
*[1024]byte传参,比传[1024]byte值更慢,因为前者强制逃逸,后者可能全程栈上
最常被忽略的是:值传递优化的前提是编译器能证明该值生命周期确定、不跨 goroutine、不被反射捕获。一旦涉及 interface{}、reflect.Value 或闭包,哪怕很小的 struct 也可能被迫堆分配。










