Go语言指针基础

指针是存储另一个变量内存地址的变量。Go语言保留了指针,但不支持指针运算(如C语言的++/--),从而减少了滥用指针带来的风险。指针在Go中主要用于:让函数能够修改外部变量(类似引用传递)、避免大数据的值拷贝、以及实现方法接收者(后续章节详述)。

关键点:Go的指针是安全的,不能进行算术运算;指针的零值为nil;使用&取地址,*解引用。

1. 指针声明与取地址

使用*类型表示指向该类型的指针。使用&操作符获取变量的内存地址。

基本示例
package main

import "fmt"

func main() {
    var num int = 42
    var ptr *int       // 声明一个指向 int 的指针,初始值为 nil
    ptr = &num         // ptr 存储 num 的地址

    fmt.Println("num 的值:", num)      // 42
    fmt.Println("num 的地址:", &num)   // 如 0xc0000180a8
    fmt.Println("ptr 的值:", ptr)      // 与 &num 相同
    fmt.Println("ptr 指向的值:", *ptr) // 解引用,输出 42
}

也可以在声明时直接初始化:ptr := &num。

2. 解引用操作符 *

通过在指针变量前加*,可以访问或修改指针指向的值。

x := 10
p := &x
fmt.Println(*p) // 读取 x 的值: 10
*p = 20        // 通过指针修改 x
fmt.Println(x) // 20

3. 指针零值 nil

未初始化的指针值为nil,不能对nil指针解引用(会导致panic)。

var p *int
fmt.Println(p == nil) // true
// fmt.Println(*p)    // panic: runtime error: invalid memory address

// 安全地解引用
if p != nil {
    fmt.Println(*p)
} else {
    fmt.Println("指针为 nil,无法解引用")
}

4. 指针作为函数参数

Go的所有参数都是值传递,但传递指针时可以修改原始变量。

func zeroVal(val int) {
    val = 0
}

func zeroPtr(ptr *int) {
    *ptr = 0
}

func main() {
    x := 100
    zeroVal(x)      // 不会修改 x
    fmt.Println(x)  // 100

    zeroPtr(&x)     // 传递指针,会修改 x
    fmt.Println(x)  // 0
}
当需要函数内修改外部变量,或者传递大结构体/切片/数组时,使用指针可以提高效率(避免拷贝大量数据)。

5. 指针与结构体

访问结构体指针的字段时,Go允许使用.直接访问,无需显式解引用(语法糖)。

type Person struct {
    Name string
    Age  int
}

func main() {
    p := Person{"Alice", 30}
    ptr := &p

    // 两种方式等价
    (*ptr).Age = 31   // 显式解引用
    ptr.Name = "Bob"  // 语法糖,自动解引用

    fmt.Println(p) // {Bob 31}
}

6. new 函数

new(T) 分配内存,返回类型为*T的指针,指向该类型的零值内存。

p := new(int)   // p 是 *int,指向的 int 值为 0
fmt.Println(*p) // 0
*p = 5
fmt.Println(*p) // 5

// 对于结构体
type Point struct{ X, Y int }
pt := new(Point)
pt.X = 10
pt.Y = 20
fmt.Println(pt) // &{10 20}
new 与 &T{} 的区别:new(T) 返回指针,且只能初始化零值;而 &T{} 可以指定非零初始值。推荐使用 &T{} 字面量方式。

7. 多级指针(指向指针的指针)

Go支持多级指针,但通常不够优雅,在实际开发中很少使用。

x := 10
p := &x      // *int
pp := &p     // **int
fmt.Println(**pp) // 10

8. 切片、map 与指针

切片和map本身已经包含了对底层数据的引用,通常不需要额外的指针。但有时需要修改切片本身(如append并需要反映到外部),可以传递切片指针。

func addElement(s *[]int, val int) {
    *s = append(*s, val)
}
nums := []int{1,2}
addElement(&nums, 3)
fmt.Println(nums) // [1,2,3]

但更推荐的做法是返回新切片。

9. 最佳实践与注意事项

  • 避免对nil指针解引用,操作前应检查指针是否为nil。
  • 不要返回指向函数局部变量的指针?实际上Go中这是允许的(逃逸到堆),没有C语言的问题。
  • 大型结构体、数组建议使用指针传递,减少拷贝。
  • 对于map、slice、channel、函数等本身是引用类型,通常不需要额外的指针。

综合示例:使用指针交换值

经典swap函数
package main

import "fmt"

func swap(a, b *int) {
    *a, *b = *b, *a
}

func main() {
    x, y := 5, 10
    fmt.Printf("交换前: x=%d, y=%d\n", x, y)
    swap(&x, &y)
    fmt.Printf("交换后: x=%d, y=%d\n", x, y)
}
交换前: x=5, y=10
交换后: x=10, y=5
小结:指针是Go语言中的重要概念,但使用范围比C语言窄得多。掌握指针的声明、取地址、解引用以及作为函数参数使用,能够更有效地控制内存和实现引用效果。下一章我们将学习结构体和方法,指针将在方法接收者中发挥关键作用。