该用 T 而不是 T 传参当对象较大(如含切片、map、chan 或字段多嵌套深)需避免拷贝,或函数需修改原值;小结构体传值更高效,接口方法含指针接收者时也必须传 T。

什么时候该用 *T 而不是 T 传参
Go 里所有参数都是值传递,但“值”本身可能是地址。判断标准很简单:对象大小是否超过几个机器字长,以及是否需要修改原值。
常见错误现象:struct 字段多、嵌套深,或含 []byte、map、chan 等大底层数组时,直接传值会触发大量内存拷贝,CPU 和 GC 压力明显上升。
- 小对象(如
type Point struct{ X, Y int })传值更高效,现代编译器还能做逃逸分析优化 - 含切片、字符串、接口或指针字段的结构体,传值只拷贝头信息(24 字节以内),实际数据不复制 —— 但这不等于“安全”,因为底层数据仍可能被意外修改
- 如果函数内部要改结构体字段(比如
user.SetLastLogin()),必须传*User,否则改的是副本
sync.Pool 能缓解大对象分配,但不能替代传参设计
有人看到大对象就下意识往 sync.Pool 里塞,结果发现逻辑变复杂、对象复用出错、甚至内存泄漏。它解决的是“高频创建销毁”,不是“传参开销”。
使用场景有限:比如 HTTP handler 中反复构造的 bytes.Buffer 或自定义解析上下文。但注意:
立即学习“go语言免费学习笔记(深入)”;
-
sync.Pool不保证对象一定复用,也不控制生命周期,Get()可能返回 nil 或脏数据 - 如果对象带状态(比如已设置的字段、打开的文件描述符),必须在
Put()前重置,否则下次Get()会拿到残留数据 - 对单次调用链中的大对象(比如从 DB 读出一个 1MB 的
map[string]interface{},再传给校验函数),sync.Pool完全不适用 —— 这时候该考虑传指针,或者重构为流式处理
接口类型传参时,interface{} 和 *T 的隐式转换陷阱
当函数接收 interface{},你传 &v 和 v 都能通过编译,但底层行为天差地别。
典型错误现象:函数里对参数做了类型断言 if x, ok := arg.(MyStruct),结果 ok 总是 false —— 因为你传的是 &v,断言目标却是值类型。
- 传
v(值)进interface{},接口底层存储的是值副本 + 类型信息 - 传
&v(指针)进interface{},接口底层存储的是指针 + 指针类型信息,断言必须写成arg.(*MyStruct) - 如果接口方法集要求指针接收者(比如
func (m *MyStruct) Save()),那么只有*MyStruct能满足该接口,MyStruct值类型不行
JSON 解析后的大结构体,别急着转指针
从 json.Unmarshal 得到一个大 struct 后,很多人立刻包一层 &result 传给下游,以为能省拷贝。其实没用 —— Unmarshal 本身已经分配了堆内存,结构体里的切片、map 都是指向堆的指针,传值只拷贝这些指针头。
真正要警惕的是后续操作:
- 如果下游函数会做深拷贝(比如
copier.Copy()或手动遍历赋值),那传指针确实能避免二次分配 - 但如果只是读字段、生成日志、拼 SQL,传值和传指针性能差异微乎其微
- 更常见的坑是:把 JSON 解析结果存在 map 或 slice 里,然后反复取
item再取地址 —— 这时&slice[i]在 slice 扩容后失效,容易 panic
复杂点在于,传参选择不是纯技术问题,它和你的 API 边界、并发安全、测试可维护性绑在一起。比如一个导出的函数签名定成 func Process(*Data),调用方就必须确保 Data 不被其他 goroutine 并发修改 —— 这个约束不会出现在函数体里,但会悄悄埋进整个调用链。









