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

new vs &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
}

AI 助手 - deepseek-v4-flash

你好!有什么可以帮你的吗?

可以问我:推荐文章、搜索主题、了解博客内容

AI 生成内容仅供参考

未播放
0:00 / 0:00