该用 *t 而不是 t 的核心标准是:需修改原值,或结构体过大导致拷贝成本高;小结构体、基本类型、不可变类型优先用值;接口中 nil 指针易致 panic,须先断言再判空;切片/map/channel 本身是引用类型,无需额外加指针。

什么时候该用 *T 而不是 T
值类型传参默认拷贝,指针传参传递地址——但这不是“能用就用”的理由。核心判断标准只有两个:是否需要修改原值,结构体是否大到拷贝成本不可忽略。
常见错误现象:函数里改了结构体字段,调用方没看到变化;或者小结构体(比如 type Point struct{ X, Y int })硬套指针,反而增加间接寻址开销。
- 必须用指针:要修改调用方的变量内容,比如
func (p *User) SetName(n string) { p.Name = n } - 推荐用指针:结构体字段超过 4–5 个基础类型,或含 slice/map/chan 等引用字段(虽然这些本身不拷贝底层数据,但结构体头信息变大)
- 优先用值:小结构体(如
time.Time、uuid.UUID)、基本类型(int、string)、或明确设计为不可变(immutable)的类型
go tool compile -gcflags="-m" 看指针逃逸的实际影响
编译器会自动决定变量是否逃逸到堆上,而指针参数常触发逃逸——但不是所有指针都导致堆分配。性能差异主要来自内存位置(栈快、堆慢)和 GC 压力,不是“指针本身慢”。
使用场景:怀疑某个函数因传指针导致意外堆分配,或想确认小结构体是否被优化掉。
立即学习“go语言免费学习笔记(深入)”;
- 运行
go tool compile -gcflags="-m -l" main.go(-l关闭内联,避免干扰判断) - 关注输出中的
... escapes to heap和leaking param: ... - 如果结构体值传参也逃逸(比如返回局部变量地址),那换指针未必改善性能
示例:传 *bytes.Buffer 进函数,大概率逃逸;但传 *int,只要不返回它或存入全局,通常不逃逸。
接口值里含指针时的坑:nil 检查失效
这是最易踩的逻辑陷阱:一个接口变量是 nil,不代表它内部的动态值也是 nil。当你把 *T 赋给接口,接口非空,即使那个指针本身是 nil。
错误现象:if myInterface == nil 为 false,但调用方法 panic:panic: runtime error: invalid memory address or nil pointer dereference。
- 正确检查方式:先类型断言,再判空,例如
if p, ok := myInterface.(*MyStruct); ok && p != nil - 避免直接将可能为
nil的指针赋给接口,尤其在构造函数或工厂函数中 - 如果接口方法允许处理
nil接收者(如func (p *T) String() string中判空),那就明确写出来,别依赖调用方防护
切片、map、channel 本身已是引用类型,别画蛇添足加指针
它们底层是包含指针的结构体(如 slice 是 struct{ ptr *T; len, cap int }),传值时拷贝的是这个结构体,不是底层数组。所以修改元素、追加元素(在 cap 允许范围内)都会反映到原变量——不需要、也不应该再包一层 *[]T。
性能影响:多一层指针意味着一次额外解引用,还可能让变量逃逸到堆。
- 错误写法:
func updateSlice(s *[]int) { *s = append(*s, 1) }—— 完全没必要 - 正确写法:
func updateSlice(s []int) []int { return append(s, 1) },调用方用s = updateSlice(s) - 唯一例外:你要在函数内重新分配整个底层数组并让调用方感知(比如彻底替换
s的ptr),那只能靠返回值,不是靠指针参数
真正需要指针的,是那些你确实要修改其 header 字段以外内容的类型,比如自定义结构体里的字段,或者需要重置的 sync.Mutex 实例。











