Go 语言:值传递 vs 指针传递
一、值传递 vs 指针传递
核心原则
默认用值传递,需要修改或避免拷贝开销时用指针。
用值传递的场景
go
// 1. 基本类型(int、float、bool、string)--- 本身就很小
func add(a, b int) int { return a + b }
// 2. 小结构体(< 100 bytes),且不需要修改
type Point struct{ X, Y float64 }
func distance(p Point) float64 { ... }
// 3. 接口值 --- 底层数据已经是指针语义
func Process(r io.Reader) { ... }
用指针传递的场景
go
// 1. 需要修改原始数据
type Counter struct { count int }
func (c *Counter) Increment() { c.count++ }
// 2. 大结构体 --- 避免拷贝开销
type BigConfig struct {
Data [4096]byte
Options map[string]string
}
func Process(cfg *BigConfig) { ... }
// 3. 可能为 nil 的值
func Handle(res *http.Response) {
if res == nil { return }
}
// 4. 一致性 --- 同类型方法接收者要统一,不要混用
关键对比
| 值传递 | 指针传递 | |
|---|---|---|
| 修改原数据 | ❌ | ✅ |
| 拷贝开销 | 有(按大小) | 无(8字节地址) |
| nil 安全 | ✅ 零值可用 | ❌ 需判空 |
| GC 压力 | 无逃逸则栈分配 | 可能逃逸到堆 |
| 并发安全 | ✅ 各自独立 | ⚠️ 共享需加锁 |
实际建议
go
// ❌ 不要这样 --- 基本类型传指针毫无意义
func add(a *int, b *int) int
// ❌ 不要为了"性能"对小结构体传指针 --- 逃逸到堆反而更慢
type Vec2 struct{ X, Y float64 }
// ✅ 粗略判断标准
// - ≤ 64 bytes 且不修改 → 值传递
// - > 64 bytes 或需要修改 → 指针传递
// - 方法接收者:一旦有一个方法用 *T,全部统一用 *T
方法接收者一致性
go
type User struct {
Name string
Age int
}
// ✅ 统一用指针 --- 因为有修改需求的方法
func (u *User) SetName(name string) { u.Name = name }
func (u *User) String() string { return u.Name } // 虽然只读,也用 *User
// ❌ 混用 --- 编译能过但容易混乱
func (u *User) SetName(name string) { u.Name = name }
func (u User) String() string { return u.Name } // 类型不同:User vs *User
一句话总结:小且不改传值,大或要改传指针,方法接收者保持一致。
二、& 和 * 详解
先理解"地址"这个概念
想象一间酒店:
- 变量 = 房间里的东西(比如 302 房间住着一个人叫张三)
- 地址 = 房间号(302)
- 指针 = 一张写着房间号的小纸条
& --- "告诉我地址在哪"
go
name := "张三"
p := &name
你问:张三住哪个房间?
&name回答:302号
& 的作用就一个:找到这个东西住在哪,把地址给你。
* --- "我要看房间里的东西"
go
name := "张三"
p := &name // p 纸条上写着 302
fmt.Println(*p) // 打印"张三"
你拿着写着 302 的纸条,推开 302 的门,看到里面是张三
* 的作用就一个:顺着地址,找到里面的东西。
可以修改!
go
name := "张三"
p := &name
*p = "李四" // 推开302的门,把里面的人换成李四
fmt.Println(name) // 打印"李四" ------ 因为 name 就住在302
改了房间里的东西,原来的变量自然就变了。
放在一起对比
go
x := 10
p := &x // p = 小纸条,上面写着 x 的地址("x住哪")
*p // 顺着纸条找到 x,读出 10("纸条指向谁")
*p = 20 // 顺着纸条找到 x,改成 20("去改纸条指向的人")
三种用法的对比
| 操作 | 符号 | 作用 | 示例 |
|---|---|---|---|
| 取地址 | &x |
获取 x 的指针 | p := &x |
| 解引用 | *p |
读写 p 指向的值 | *p = 10 |
| 声明 | *int |
声明指针类型 | var p *int |
怎么记?
| 符号 | 一句话 | 记忆口诀 |
|---|---|---|
& |
给我地址 | & = address = 地址 |
* |
顺着地址找内容 | * = 指向里面的东西 |
常见模式
go
// 1. 函数返回指针 --- 避免大对象拷贝
func NewUser(name string) *User {
return &User{Name: name} // & 取局部变量地址,Go 会逃逸到堆
}
// 2. 通过指针修改
func reset(val *int) { *val = 0 }
n := 99
reset(&n) // n 变成 0
// reset(n) // ❌ 编译错误:不能传 int 给 *int
// 3. 结构体指针可以直接访问字段 --- Go 自动解引用
u := &User{Name: "Tom"}
u.Name = "Jerry" // 等价于 (*u).Name
常见错误
go
var p *int // 一张空纸条,上面什么都没写(nil)
fmt.Println(*p) // ❌ panic!你拿着一张空纸条去找房间,找不到
空纸条不能推门,开门前先看看纸条是不是空的。
go
x := 42
f(x) // ❌ 编译错误:函数要的是纸条,你递过去一个真人
f(&x) // ✅ 把地址(纸条)递过去
函数说要纸条你就给纸条,别直接把人塞过去。
最终总结:& 查地址,* 查内容。小且不改传值,大或要改传指针。