传大结构体要用指针而非值拷贝,因值传递会复制整个对象,造成显著性能开销和GC压力;指针传递避免拷贝,但需防nil panic和意外修改,且含sync.Mutex等不可拷贝字段时必须用指针。

为什么传大结构体要用指针而不是值拷贝
Go 中函数参数是值传递,传 struct 时会复制整个对象。如果结构体包含大量字段、大数组或嵌套切片(比如 []byte 占几 MB),拷贝开销显著,GC 压力也会增大。即使结构体里只含指针(如 slice、map、string),其头部(sliceHeader 等)仍会被复制,但真正数据不会——可一旦结构体本身很大(例如 100 字段的 struct),光复制头部就慢。
- 典型“大对象”:含
[]byte、map[string]interface{}、多层嵌套 struct、自定义大数组(如[1024 * 1024]byte) - 值传参时,编译器无法省略拷贝,哪怕你只读不写
- 用指针传参后,函数内访问字段需加
*或直接用obj.Field(Go 自动解引用)
怎么安全地用指针传参:避免 nil panic 和意外修改
传 *T 比传 T 快,但引入两个常见风险:nil 指针解引用 panic,以及函数内部意外修改原对象。必须显式防御。
- 函数开头立即检查
if obj == nil { return errors.New("obj is nil") },不要等第一次访问字段才 panic - 若函数只读不改,用
const思维对待参数:文档注明 “read-only”,并在函数内避免赋值给obj.Field;Go 不支持const *T,只能靠约定+review - 若结构体含
sync.Mutex等非可拷贝字段,必须用指针——否则编译报错cannot be copied
func processUser(u *User) error {
if u == nil {
return errors.New("u must not be nil")
}
// Go 允许直接 u.Name,无需 (*u).Name
log.Printf("processing %s", u.Name)
return nil
}性能差异实测:什么时候值得改用指针
不是所有 struct 都需要指针优化。小结构体(字节)在现代 CPU 上拷贝极快,用指针反而可能因额外内存访问(cache miss)变慢。重点看实际 size 和调用频次。
- 用
unsafe.Sizeof(T{})查真实大小,注意字段对齐(如bool后跟int64可能 padding 出 16 字节) - 高频调用(如 HTTP handler 每请求一次)且 struct > 64 字节,指针收益明显
- 用
go test -bench=.对比func f(v BigStruct)和func f(p *BigStruct)的 ns/op
type BigPayload struct {
ID int64
Data [1024 * 1024]byte // 1MB
Labels map[string]string
}
// unsafe.Sizeof(BigPayload{}) ≈ 1MB + pointer overhead → 必须用 *BigPayload接口参数中隐藏指针:接收方无感但底层高效
当函数参数类型是接口(如 io.Reader、自定义 Processor),实现类型用指针还是值,调用方可以完全无感——但实现侧必须一致。这是隐藏优化的好位置。
立即学习“go语言免费学习笔记(深入)”;
- 如果实现 struct 很大,它的方法集应定义在
*T上(而非T),否则传值时会拷贝再转接口,白费功夫 - 接口变量本身只存 type + value(8~16 字节),但若 value 是大 struct,装箱时仍会拷贝;而装箱
*T只拷贝指针 - 检查方法集:运行
go doc yourpkg.YourType,确认func (*YourType) Method()是否存在
容易被忽略的是:哪怕你写了 func (t *T) Read(...),如果调用处写 var t T; doSomething(t)(传值),Go 会拷贝 t 再取地址,导致一次无谓拷贝。必须传 &t 或声明变量为 *T。











