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