Go 里的指针不复杂,但非常实用,不像 C/C++ 那么危险,也不像 Java 那样完全看不到。
一、Go 指针是什么
1. 指针的本质
指针 = 变量的内存地址
var a int = 10 var p *int = &aa:值是10&a:a 的地址p:指向a的指针*p:通过指针访问a的值
fmt.Println(a) // 10 fmt.Println(p) // 0xc00001a0a8(地址) fmt.Println(*p) // 10口诀:
& 取地址,解引用*
2. Go 指针的“安全边界”
和 C/C++ 不同,Go:
不能指针运算(
p++不存在)不能随便转类型
有 GC(不用手动 free)
空指针是
nil
var p *int fmt.Println(p == nil) // true二、Go 指针使用场景
1. 修改函数外的变量(非常核心)
❌ 值传递(改不到外面):
func add(a int) { a++ } func main() { x := 10 add(x) fmt.Println(x) // 10 }✅ 指针传递:
func add(a *int) { *a++ } func main() { x := 10 add(&x) fmt.Println(x) // 11 }Go 只有值传递,但“指针的值”可以指向同一块内存
2 结构体 + 指针(Go 的高频用法)
type User struct { Name string Age int } func grow(u *User) { u.Age++ } func main() { u := User{Name: "Tom", Age: 18} grow(&u) fmt.Println(u.Age) // 19 }注意: Go 一个很贴心的地方:
u.Age++ // 等价于 (*u).Age++Go 自动帮解引用,不需要满屏*
3. new / & 的区别
p1 := new(int) // *int,值是 0 p2 := &User{} // *User等价写法:
var a int p := &a一般习惯:
基本类型:
&结构体:
&User{}或构造函数
三、Go 指针的核心使用场景
场景 1:需要修改对象本身(最常见)
func updateName(u *User) { u.Name = "Jack" }场景 2:避免大对象拷贝(性能 & 内存)
type BigStruct struct { Data [100000]int } func process(b *BigStruct) { // 不拷贝 100000 个 int }场景 3:区分“没传”和“传了零值”
这个在 API / JSON / DB 特别重要
type Req struct { Age *int `json:"age"` }nil→ 前端没传0→ 前端明确传了 0
场景 4:方法接收者用指针(Go 面向对象)
func (u *User) Grow() { u.Age++ }什么时候用指针接收者?
需要修改对象
结构体比较大
保证方法一致性(推荐)
官方建议:一个结构体,要么全指针接收者,要么全值接收者
场景 5:与 interface 配合
type Writer interface { Write() } type File struct{} func (f *File) Write() {} var w Writer w = &File{} // 正确这样不行,因为方法在*File上:
w = File{} // 没实现接口场景 6:并发 & 共享状态
需谨慎使用
var count int var mu sync.Mutex func inc() { mu.Lock() count++ mu.Unlock() }虽然不是“显式指针”,但底层都是共享内存 + 地址
四、Go 指针 vs Java/C++
| 对比 | Go | Java | C++ |
|---|---|---|---|
| 手动内存 | ❌ | ❌ | ✅ |
| 指针运算 | ❌ | ❌ | ✅ |
| 空指针 | nil | null | nullptr |
| 参数传递 | 值传递 | 值传递(引用语义) | 值/引用 |
Go 指针 = “受控版 C 指针 + Java 引用的灵活性”
五、新手常见坑
❌ 对 map / slice 再取指针
func f(m *map[string]int) // 一般没必要因为:
map / slice 本身就是“引用类型”
直接传就能改
❌ nil 指针解引用
var u *User u.Age = 10 // panic一定要先初始化。
六、总结
Go 指针的目标只有三个:
修改原数据
减少拷贝
表达“可选值”