
传大结构体时直接用指针,别犹豫
Go 里结构体默认按值传递,一复制就是整块内存拷贝。如果结构体含切片、map、string 或嵌套大量字段(比如 type User struct{ ID int; Name string; Profile map[string]interface{}; Posts []Post }),传参或赋值时开销肉眼可见。直接把参数类型从 User 改成 *User,函数内用 u.Name 而不是 (*u).Name(Go 自动解引用),就能避开拷贝。
常见错误现象:函数接收大结构体后 CPU 突增、GC 频繁、pprof 显示大量堆分配 —— 很可能就是值传递在反复 alloc。
- 只要不修改原结构体语义,且结构体大小 > 机器字长(通常 8 字节),就该优先传指针
- 注意:指针传递后,函数内对字段的修改会反映到原变量;若需隔离修改,仍得值拷贝
- 接口类型(如
io.Reader)本身已含指针语义,不用额外加*
方法集绑定:值接收者 vs 指针接收者
给结构体定义方法时,接收者用值还是指针,直接影响能否调用该方法——尤其当结构体变量是值类型时。比如 var u User,若方法是 func (u *User) Save(),则 u.Save() 合法(Go 自动取地址);但若变量是 var u *User,而方法是 func (u User) Clone(),则 u.Clone() 也合法(Go 自动解引用)。真正卡住的是接口赋值场景。
使用场景:实现 fmt.Stringer、json.Marshaler 等接口时,若结构体较大,用指针接收者可避免 marshal 过程中无谓拷贝。
立即学习“go语言免费学习笔记(深入)”;
- 值接收者方法:不能修改原值,且对大结构体有拷贝成本
- 指针接收者方法:可修改原值,零拷贝,但要求调用方能提供地址(即不能对字面量或临时值直接调用,如
User{}.Save()报错) - 混用风险:一个类型既有值接收者方法又有指针接收者方法,可能导致方法集不一致,影响接口实现判断
sync.Pool 里存指针,但别存指向栈的指针
用 sync.Pool 复用大结构体实例时,必须确保放进去的是堆分配对象。Go 编译器会对逃逸分析失败的变量做栈分配,若误将栈地址存入 sync.Pool(比如 pool.Put(&localVar)),后续 Get 可能拿到已失效内存,引发 panic 或数据错乱。
常见错误现象:fatal error: unexpected signal during runtime execution 或随机字段为零值。
- 安全做法:用
new(T)或&T{}显式分配堆内存,再放入 Pool - 验证是否逃逸:加
go build -gcflags="-m -l",看变量是否标注escapes to heap - Pool 本身不解决内存开销,只减少 GC 压力;若结构体含大量子对象(如嵌套 map),需配合预分配(如
make(map[int]string, 1024))才有效
JSON 解析时用指针字段避免重复分配
解析 JSON 到结构体时,如果字段声明为值类型(如 Name string),每次反序列化都会新建字符串并拷贝内容;若改为指针(Name *string),Go 的 encoding/json 在遇到空字段时设为 nil,非空时才分配,且复用底层字节切片(通过 unsafe.String 优化)。这对含大量可选字段的大 payload 很关键。
性能影响:实测某 50KB JSON,字段全用指针比全用值类型减少约 30% 分配次数和 20% 时间开销。
- 必须配合
omitempty标签使用,否则 nil 指针会序列化成 null,破坏 API 兼容性 - 注意:
*string无法直接比较相等,需先判空再比值;sql.NullString等类型已有类似设计,可参考 - 不要滥用:简单结构体(如仅几个 int/string)没必要,反而增加解引用开销和代码复杂度










