必须用*t接收者仅在两种场景:方法需修改receiver字段,或receiver含不可拷贝字段(如sync.mutex);值接收者无法修改原值,且结构体是否传指针取决于字节数(≤8字节传值更优,≥16字节建议用指针)。

什么时候必须用指针接收者
必须用 *T 接收者,只在两个场景:方法要修改 receiver 字段,或 receiver 包含不可拷贝字段(如 sync.Mutex)。值接收者 T 永远改不了原值——哪怕结构体只有 2 字节,u.Age++ 在值接收者里也白干。
常见错误现象:counter.Inc() 调用后 counter.Count 没变;或 struct{ mu sync.Mutex } 类型的方法用值接收者,编译直接报错 cannot use s (type S) as type sync.Locker in argument to lock: S does not implement sync.Locker (Lock method has pointer receiver)。
结构体多大才值得传指针
别看字段个数,看总字节数。≤ 8 字节(如 struct{ x, y int64 })传值更快;≥ 16 字节(比如含 string、[]byte、map[string]int 的结构体)就该用指针。中间值(9–15 字节)要看调用频次:高频调用(如 HTTP handler 内循环)建议统一用指针,避免累积开销。
-
time.Time是 24 字节,net.IP是 16 字节,都该用*T - 字段含
slice/map/chan/func/interface{}—— 即使结构体本身小,这些 header 拷贝也有成本,且语义上常需修改内部内容 - 用
go tool compile -S your_file.go | grep MOVQ看是否真在传地址;若函数被内联,传值反而没额外指令
接口实现时指针和值接收者的坑
接口赋值是隐式类型检查,不是自动升级。如果类型 MyStruct 只有 (*MyStruct).Write() 方法,那么 var w io.Writer = MyStruct{} 会编译失败:值类型不满足接口。反过来,如果只有 (MyStruct).Read(),那 var r io.Reader = &MyStruct{} 也会失败。
实操建议:
- 用
var _ io.Writer = (*MyStruct)(nil)在包初始化时做静态断言,提前暴露问题 - 别混用:一个类型既有
(T)方法又有(*T)方法,会导致方法集分裂——T值能调部分方法,*T能调全部,但接口判断只认完整方法集 - map 或 channel 中存结构体时,优先存
*MyStruct;否则转成interface{}时可能触发隐式取址失败(如 map 中的值不可寻址)
sync.Pool 和指针接收者的配合要点
sync.Pool 存指针是常规操作,但关键在 Reset 方法:它必须清空所有可变字段,否则复用对象会带脏数据。例如 bytes.Buffer 的 Reset() 会置空底层数组,但如果你自己写 (*MyStruct).Reset() 只清了 id 没清 data []byte,下次 Get 到的就是残留数据。
另一个容易忽略的点:sync.Pool.Put(&localVar) 是危险的——localVar 是栈变量,地址逃逸后可能被回收。必须确保 Put 的是堆分配对象,比如 new(MyStruct) 或 &MyStruct{}(后者由编译器决定是否逃逸,加 -gcflags="-m" 确认)。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











