Golang - 3.指针
Go 指针详解
1. 什么是指针
指针是一个变量,其值为另一个变量的内存地址。通过指针,可以直接访问和修改存储在该地址上的数据。
变量名 → 值
| |
v v
x = 42 ← 变量 x 存储值 42
&x = 0xc0000b2008 ← 变量 x 的内存地址
p = 0xc0000b2008 ← 指针 p 存储的是 x 的地址
*p = 42 ← 通过 *p 解引用,获取地址上的值
2. 指针基础操作
2.1 取地址运算符 &
使用 & 获取变量的内存地址:
package main
import "fmt"
func main() {
x := 42
fmt.Println(&x) // 输出: 0xc0000b2008 (x 的内存地址)
}
2.2 指针声明与零值
使用 *T 声明一个指向 T 类型的指针,零值为 nil:
var p *int // 声明一个 *int 类型的指针,初始值为 nil
fmt.Println(p) // <nil>
fmt.Println(p == nil) // true
2.3 指针初始化
x := 42
p := &x // p 是指向 x 的指针,类型为 *int
fmt.Println(p) // 输出地址,如 0xc0000b2008
fmt.Println(*p) // 输出: 42 (解引用)
2.4 解引用运算符 *
通过 *指针 访问指针指向的值:
x := 42
p := &x
fmt.Println(*p) // 42 — 读取指针指向的值
*p = 100 // 通过指针修改值
fmt.Println(x) // 100 — x 的值被修改了
3. 指针的使用场景
3.1 函数参数传递(避免值拷贝)
Go 中函数参数默认是值传递,指针可以在函数内部修改外部变量:
// ❌ 值传递:无法修改外部变量
func add1(x int) {
x = x + 1 // 只修改了副本
}
// ✅ 指针传递:可以修改外部变量
func add1Ptr(x *int) {
*x = *x + 1 // 修改指针指向的值
}
func main() {
a := 10
add1(a)
fmt.Println(a) // 10,没有变化
add1Ptr(&a)
fmt.Println(a) // 11,被修改了
}
3.2 大结构体传参(提升性能)
对于大型结构体,传指针避免了整块内存的拷贝:
type BigStruct struct {
Data [1000000]int
}
// ❌ 每次调用都拷贝整个结构体(约 8MB)
func process(b BigStruct) { /* ... */ }
// ✅ 只拷贝一个地址(8 字节)
func processPtr(b *BigStruct) { /* ... */ }
3.3 实现方法修改接收者
type Counter struct {
count int
}
// ❌ 值接收者:无法修改原始数据
func (c Counter) Increment() {
c.count++ // 只修改副本
}
// ✅ 指针接收者:可以修改原始数据
func (c *Counter) IncrementPtr() {
c.count++ // 修改原始数据
}
func main() {
c := Counter{count: 0}
c.Increment()
fmt.Println(c.count) // 0
c.IncrementPtr()
fmt.Println(c.count) // 1
}
4. 指针与复合类型
4.1 指针与结构体
type Person struct {
Name string
Age int
}
p := &Person{Name: "Alice", Age: 30}
// 两种访问字段的方式等价
fmt.Println((*p).Name) // Alice
fmt.Println(p.Name) // Alice — Go 自动解引用
4.2 指针与数组
arr := [3]int{1, 2, 3}
p := &arr
fmt.Println((*p)[0]) // 1
fmt.Println(p[0]) // 1 — Go 自动解引用数组指针
4.3 指针与切片
切片本身已包含指向底层数组的指针,通常不需要再取切片的指针:
s := []int{1, 2, 3}
// 切片本身是引用类型,函数内修改切片元素会影响原始切片
func modifySlice(s []int) {
s[0] = 100
}
4.4 指针与 Map
Map 也是引用类型,无需使用指针:
m := map[string]int{"a": 1}
func modifyMap(m map[string]int) {
m["b"] = 2 // 直接修改
}
5. new 函数
new(T) 分配零值内存并返回指针:
p := new(int) // 分配内存,*int 类型,值为 0
fmt.Println(*p) // 0
*p = 42
fmt.Println(*p) // 42
newvs&T{}:对于结构体,推荐使用&T{},更直观且可初始化字段。
// 两种等价方式
p1 := new(Person) // 零值初始化
p2 := &Person{} // 零值初始化
p3 := &Person{Name: "Bob"} // 带初始值
6. 指针的指针
指针也可以指向另一个指针:
x := 42
p := &x // p *int → x
pp := &p // pp **int → p
fmt.Println(x) // 42
fmt.Println(*p) // 42
fmt.Println(**pp) // 42
**pp = 100
fmt.Println(x) // 100
7. nil 指针与安全性
7.1 nil 指针解引用会 panic
var p *int
fmt.Println(*p) // ❌ panic: runtime error: invalid memory address or nil pointer dereference
7.2 安全使用指针
func safeDeref(p *int) int {
if p == nil {
return 0 // 默认值
}
return *p
}
func main() {
var p *int
fmt.Println(safeDeref(p)) // 0,安全
}
8. 指针与接口
值类型和指针类型都可以实现接口,但需要注意区别:
type Speaker interface {
Speak()
}
type Dog struct {
Name string
}
// 值接收者:值和指针都能调用
func (d Dog) Speak() {
fmt.Println(d.Name + " says woof!")
}
// 指针接收者:只有指针类型实现了接口
// func (d *Dog) Speak() {
// fmt.Println(d.Name + " says woof!")
// }
func main() {
d := Dog{Name: "Rex"}
var s1 Speaker = d // ✅ 值接收者时可用
var s2 Speaker = &d // ✅ 值接收者时也可用
s1.Speak()
s2.Speak()
}
规则:如果方法使用了指针接收者,那么只有
*Dog实现了Speaker接口,Dog不满足。
9. unsafe.Pointer
unsafe.Pointer 可以绕过类型系统进行指针转换,属于不安全操作:
import "unsafe"
func main() {
x := 42
// *int → unsafe.Pointer → *float64
p := (*float64)(unsafe.Pointer(&x))
fmt.Println(*p) // 输出浮点解读的二进制值,非 42.0
}
注意:仅在极少数需要底层操作时使用,常规开发应避免。
10. 常见陷阱与最佳实践
陷阱 1:循环变量指针
// ❌ 错误:所有指针指向同一变量
func wrong() {
var ptrs []*int
for i := 0; i < 3; i++ {
ptrs = append(ptrs, &i) // i 是循环变量,地址不变
}
for _, p := range ptrs {
fmt.Println(*p) // 3, 3, 3
}
}
// ✅ 正确:每次迭代创建新变量
func right() {
var ptrs []*int
for i := 0; i < 3; i++ {
v := i // 新变量
ptrs = append(ptrs, &v)
}
for _, p := range ptrs {
fmt.Println(*p) // 0, 1, 2
}
}
Go 1.22+ 已修复此问题,循环变量每次迭代都会创建新实例。
陷阱 2:返回局部变量指针
// ✅ Go 中安全:编译器会逃逸分析,将变量分配到堆上
func newInt() *int {
x := 42
return &x // 安全,x 会被分配到堆上
}
陷阱 3:指针比较
a := 1
b := 1
pa := &a
pb := &b
fmt.Println(pa == pb) // false — 不同变量,地址不同
pc := &a
fmt.Println(pa == pc) // true — 指向同一变量
最佳实践总结
| 实践 | 说明 |
|---|---|
| 结构体方法需要修改自身时,使用指针接收者 | func (c *Counter) Inc() |
| 大结构体传参用指针 | 避免拷贝开销 |
| 切片、Map、Channel 不需要再取指针 | 它们本身就是引用类型 |
| 解引用前检查 nil | 避免空指针 panic |
优先使用 &T{} 而非 new(T) |
更清晰,可初始化字段 |
| 避免滥用指针 | 值类型更安全、更简单 |
11. 值类型 vs 引用类型
| 类型 | 赋值行为 | 是否需要指针修改 |
|---|---|---|
| int, float, string, bool, array | 值拷贝 | 是 |
| struct | 值拷贝 | 是(需要修改时) |
| slice | 引用底层数组 | 通常不需要 |
| map | 引用底层数据 | 不需要 |
| channel | 引用底层数据 | 不需要 |
| pointer | 值拷贝(拷贝地址) | — |
12. 完整示例
package main
import "fmt"
type Rect struct {
Width, Height float64
}
// 指针接收者:可以修改原始数据
func (r *Rect) Scale(factor float64) {
r.Width *= factor
r.Height *= factor
}
// 值接收者:不修改原始数据
func (r Rect) Area() float64 {
return r.Width * r.Height
}
// 指针参数:修改外部变量
func double(n *int) {
*n *= 2
}
func main() {
// 基本指针操作
x := 10
p := &x
fmt.Printf("x = %d, &x = %p, *p = %d\n", x, p, *p)
*p = 20
fmt.Printf("修改后 x = %d\n", x)
// 函数传指针
double(&x)
fmt.Printf("double 后 x = %d\n", x) // 40
// 结构体指针
r := &Rect{Width: 3, Height: 4}
fmt.Printf("面积 = %.2f\n", r.Area()) // 12.00
r.Scale(2)
fmt.Printf("缩放后 = %.2f x %.2f, 面积 = %.2f\n",
r.Width, r.Height, r.Area()) // 6.00 x 8.00, 48.00
}