Go函数参数全是值传递,传指针能改原值是因为传入的是地址副本,解引用后写入同一内存;是否用指针取决于类型大小、是否需修改、并发需求和接口约束四大条件。

Go函数参数全是值传递,传指针能改原值不是因为“支持引用”,而是因为你传进去的副本里存的是同一块内存地址;想安全、高效、不踩坑,得看类型大小、是否需修改、并发场景和接口约束这四个硬条件。
为什么func f(x *int)能改外面的变量,但func f(x int)不能
因为f(x int)收到的是整数 42 的拷贝,改它等于在纸上抄一遍数字再涂改——原纸条没动;而f(x *int)收到的是地址 0xc00001a080 的拷贝,这个地址指向原始变量所在的位置,解引用 *x = 99 就是直接往那块内存写。别被“指针能改”带偏,本质还是值传:你传的是地址这个值,不是语言开了后门。
- 常见错误现象:
p.Name = "new"写完主函数里还是旧值 → 检查函数签名漏没漏*,调用时忘没忘& - 底层机制:所有参数都拷贝一次,
*T拷的是 8 字节地址,T拷的是整个结构体(比如 2KB 的 config 就真拷 2KB) - 性能影响:小 struct(如
type Vec2 {X, Y float64})值传更快;大 struct(含[]byte、map[string]int)传指针可避免 runtime.mallocgc 上升
slice、map、chan 为什么传值也能改内容
它们不是引用类型,而是「含指针的值类型」:传 slice 时拷贝的是它的 header(ptr、len、cap 三字段),其中 ptr 指向底层数组。所以 s[0] = 999 能改原数组,但 s = append(s, x) 可能分配新数组,这时原 s 不变,必须用返回值 s = append(s, x)。
- 容易踩的坑:
*[]T或*map[K]V是多余操作——header 本身已含共享语义,再套指针只增解引用开销和nil判空负担 - 典型错误示例:
var s []string; setVal(s)中setVal内部s = []string{"a"}不会影响外部s;必须传&s并在函数内写*s = []string{"a"} - 并发注意:
s[0] = x和append同时发生可能 panic,不是线程安全的,需加锁或用原子操作
什么时候必须用*T,而不是T
不是“想改就加星号”,而是不加就做不到:
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
立即学习“go语言免费学习笔记(深入)”;
- 需要修改调用方变量本身:比如
swap(a, b *int),不传指针根本没法交换两个变量的值 - 结构体字段多、含大
[]byte、map[string]int或嵌套 struct:值传会复制整个 header + 所有字段值,压测可见 GC 压力明显上升 - 方法接收者要更新字段:
func (u *User) SetName(n string)是标准写法;用func (u User)写,u.Name = n只改副本,外部不变 -
json.Unmarshal等函数要求v interface{}实际指向可寻址值,传&v才生效,传v会静默失败或 panic
传*T最容易忽略的三个坑
传指针不是万能钥匙,搞错就 crash 或逻辑错:
-
nil *T解引用必 panic:var p *string; fmt.Println(*p)直接终止程序;调用前得判空,或用if p != nil - 栈变量取地址后逃逸:局部变量
s := MyStruct{},传&s可能导致它从栈挪到堆,GC 压力上升;小 struct 值传更高效 - 并发读写没加锁:多个 goroutine 同时通过
*T修改同一结构体字段,不加sync.Mutex或原子操作,结果不可预测
真正复杂的地方不在语法,而在判断:一个 struct 到底算“大”还是“小”,不能只看字段数量,得看它是否含引用类型字段;而一旦某个方法用了指针接收者,整个类型的使用方式就得统一,否则接口实现会失败——这点常被跳过测试直接上线出问题。

















