导航菜单

指针基础

指针(Pointer)

指针是存储另一个变量内存地址的变量。通过指针,可以直接访问和修改内存中的值。Go 语言保留了指针以提高程序效率,但移除了 C 语言中容易出错的指针运算功能,使指针更加安全。

& 与 * 运算符

Go 提供两个基本的指针运算符:

运算符名称作用示例
&取地址运算符获取变量的内存地址p := &x
*解引用运算符通过指针访问其指向的值y := *p

基本用法

func main() {
    x := 42

    // & 取地址:获取 x 的内存地址
    p := &x
    fmt.Printf("x 的值: %d\n", x)    // x 的值: 42
    fmt.Printf("x 的地址: %p\n", p)  // x 的地址: 0xc00001a0a8(每次运行可能不同)
    fmt.Printf("p 的类型: %T\n", p)  // p 的类型: *int

    // * 解引用:通过指针访问其指向的值
    fmt.Printf("*p 的值: %d\n", *p)  // *p 的值: 42

    // 通过指针修改值
    *p = 100
    fmt.Printf("x 的值: %d\n", x)    // x 的值: 100
}

指针的零值

指针的零值是 nil,表示不指向任何内存地址:

var p *int
fmt.Println(p)      // <nil>
fmt.Println(p == nil)  // true

// 注意:对 nil 指针解引用会导致 panic
// *p = 42  // panic: runtime error: invalid memory address or nil pointer dereference
func safeDeref(p *int) int {
    if p == nil {
        return 0  // 或返回错误
    }
    return *p
}

指针作为函数参数

值传递 vs 引用传递

Go 中所有函数参数都是值传递。传递指针时,实际上传递的是地址值的拷贝,但通过这个拷贝的地址仍然可以修改原始数据。这实现了类似”引用传递”的效果,但本质上仍然是值传递。

值传递的问题

// 值传递:函数接收的是参数的副本
func tryDouble(x int) {
    x = x * 2  // 只修改了副本,不影响原始变量
}

func main() {
    num := 10
    tryDouble(num)
    fmt.Println(num)  // 10(没有变化!)
}

使用指针参数修改原始值

// 指针参数:函数接收地址的副本,通过地址修改原始值
func double(x *int) {
    *x = *x * 2  // 通过指针修改原始值
}

func main() {
    num := 10
    double(&num)  // 传递 num 的地址
    fmt.Println(num)  // 20(原始值被修改了!)
}

指针参数的实际应用

// 1. 交换两个变量
func swap(a, b *int) {
    *a, *b = *b, *a
}

func main() {
    x, y := 10, 20
    swap(&x, &y)
    fmt.Println(x, y)  // 20 10
}

// 2. 修改结构体字段
type User struct {
    Name string
    Age  int
}

func birthday(u *User) {
    u.Age++  // 通过指针修改结构体
}

func main() {
    user := User{"Alice", 25}
    birthday(&user)
    fmt.Printf("%+v\n", user)  // {Name:Alice Age:26}
}

// 3. 避免大结构体的拷贝
func processLargeData(data *[]byte) {
    // 传递切片的指针,避免拷贝底层数组
    (*data)[0] = 'H'
}

new() 函数

new() 函数

new(T) 是 Go 的内置函数,它为类型 T 分配零值内存,并返回指向该内存的指针 *T。等价于 var p *T = new(T)p := new(T)

基本用法

// 使用 new() 创建指针
p := new(int)     // 分配一个 int 大小的内存,初始值为 0
fmt.Println(*p)   // 0
fmt.Println(p)    // 0xc00001a0a8(内存地址)

*p = 42
fmt.Println(*p)   // 42

new() 与变量声明的对比

// 方式 1:var 声明
var p1 *int = new(int)

// 方式 2:简短声明
p2 := new(int)

// 方式 3:先声明变量,再取地址
var x int
p3 := &x

// 以上三种方式 p1、p2、p3 都是指向 int 的指针

new() 与结构体

type Point struct {
    X, Y float64
}

// 使用 new()
p := new(Point)
fmt.Printf("%+v\n", p)  // &{X:0 Y:0}

// 等价于
// var p *Point = new(Point)

// 更常用的方式:使用结构体字面量
p2 := &Point{X: 3.0, Y: 4.0}
fmt.Printf("%+v\n", p2)  // &{X:3 Y:4}

new() 与 make() 的区别

// new() 对于切片、映射、通道不太实用
p := new([]int)       // p 是 *[]int,指向一个 nil 切片
// (*p)[0] = 1        // panic:不能对 nil 切片进行操作

// 正确做法:使用 make()
s := make([]int, 5)   // s 是 []int,长度为 5,元素为零值
s[0] = 1              // 正确

m := make(map[string]int)  // 创建并初始化 map
m["key"] = 1               // 正确

ch := make(chan int, 10)    // 创建带缓冲的通道
特性new(T)make(T)
返回类型*T(指针)T(值本身)
初始值类型零值已初始化(可立即使用)
适用类型所有类型slicemapchan
典型用途创建结构体指针创建引用类型

Go 指针 vs C 指针

Go 保留了指针以提高效率,但做了重大简化以确保安全性:

特性Go 指针C 指针
指针运算不支持✅ 支持 p++p+1
指针算术❌ 不支持✅ 支持任意偏移量运算
强制类型转换❌ 不允许✅ 允许 int*char*
空指针检查编译器部分检查完全依赖程序员
垃圾回收✅ 由 GC 管理❌ 需手动管理
安全性低(容易产生内存问题)

不支持指针运算

arr := [5]int{10, 20, 30, 40, 50}
p := &arr[0]

// 以下操作在 Go 中都是非法的:
// p++        // 编译错误:cannot increment p
// p + 1     // 编译错误
// *(p + 2)  // 编译错误

// 正确做法:使用切片和索引
fmt.Println(arr[2])  // 30

不支持指针强制转换

var x int = 42
var p *int = &x

// 以下操作在 Go 中都是非法的:
// var pp *float64 = (*float64)(p)  // 编译错误
// var n int = int(p)                // 编译错误

// 正确做法:使用显式类型转换
var y float64 = float64(x)  // 值的类型转换,不是指针转换
import "unsafe"

// 仅作演示,不推荐在生产代码中使用
var x int = 42
var p *int = &x
var pFloat *float64 = (*float64)(unsafe.Pointer(p))
// *pFloat 可以访问 x 的内存,但这非常危险!

值传递 vs 引用传递

Go 的参数传递机制

Go 中所有的参数传递都是值传递。无论是基本类型、结构体还是指针,传递的都是值的拷贝。

值传递示例

type Person struct {
    Name string
    Age  int
}

// 值传递:接收结构体的副本
func modifyValue(p Person) {
    p.Name = "Modified"  // 只修改副本
    p.Age = 100
}

// 指针传递(本质还是值传递,传递的是地址的副本)
func modifyPointer(p *Person) {
    p.Name = "Modified"  // 通过地址修改原始数据
    p.Age = 100
}

func main() {
    person := Person{Name: "Alice", Age: 25}

    modifyValue(person)
    fmt.Printf("%+v\n", person)  // {Name:Alice Age:25}(未改变)

    modifyPointer(&person)
    fmt.Printf("%+v\n", person)  // {Name:Modified Age:100}(已改变)
}

内存模型图解

值传递:
┌─────────────────┐         ┌─────────────────┐
│ main()          │         │ modifyValue()   │
│                 │         │                 │
│ person ─────────┼────────>│ p (副本)        │
│ {Name:Alice     │         │ {Name:Alice     │
│  Age:25}        │         │  Age:25}        │
└─────────────────┘         └─────────────────┘
                        两个独立的内存空间

指针传递:
┌─────────────────┐         ┌─────────────────┐
│ main()          │         │ modifyPointer() │
│                 │         │                 │
│ person ─────────┼──┐      │ p (地址副本)    │
│ {Name:Alice     │  └─────>│ ────────┐       │
│  Age:25}        │         │         │       │
└─────────────────┘         └─────────┼───────┘

                          通过地址修改同一块内存

Go 中的”引用类型”

Go 中有一些类型在传递时看似”引用传递”,但它们的本质是包含底层引用的结构:

类型描述传递行为
slice切片传递包含指针的 slice header
map映射传递指向底层哈希表的指针
channel通道传递指向底层队列的指针
interface接口传递包含类型和数据的 iface 结构
// 切片传递
func modifySlice(s []int) {
    s[0] = 999  // 可以修改元素(因为 slice header 中有底层数组的指针)
    s = append(s, 4)  // append 可能创建新数组,不影响原始切片
}

func main() {
    nums := []int{1, 2, 3}
    modifySlice(nums)
    fmt.Println(nums)  // [999 2 3](第一个元素被修改了)
}
// 安全的做法:传递切片指针
func safeAppend(s *[]int, val int) {
    *s = append(*s, val)
}

func main() {
    nums := []int{1, 2, 3}
    safeAppend(&nums, 4)
    fmt.Println(nums)  // [1 2 3 4]
}

练习题

练习 1:指针基础操作

以下代码的输出是什么?请逐步分析。

package main

import "fmt"

func main() {
    a := 10
    b := 20

    p1 := &a
    p2 := &b

    *p1 = *p2
    p2 = p1

    fmt.Println(a, b)
    fmt.Println(*p1, *p2)
}
参考答案

解题思路:分步跟踪每个变量的值。

分析

  1. a := 10, b := 20 → a=10, b=20
  2. p1 := &a → p1 指向 a
  3. p2 := &b → p2 指向 b
  4. *p1 = *p2 → 通过 p1 修改 a 的值为 b 的值(20),所以 a=20
  5. p2 = p1 → p2 现在也指向 a(b 没有被修改,仍为 20)

输出

20 20
20 20

解释

  • *p1 = *p2:将 p2 指向的值(20)赋给 p1 指向的内存位置(即 a),所以 a 变为 20
  • p2 = p1:将 p1 的地址值赋给 p2p2 现在指向 a
  • 最终 a=20b=20b 始终没有被修改),*p1=20*p2=20(都指向 a

练习 2:指针参数交换值

编写函数 rotate3(a, b, c *int),将三个变量的值循环左移。例如原始值为 (1, 2, 3),调用后变为 (2, 3, 1)

参考答案

解题思路:通过指针参数直接修改三个变量的值,使用临时变量暂存一个值。

代码

package main

import "fmt"

func rotate3(a, b, c *int) {
    temp := *a  // 暂存 a 的值
    *a = *b     // a ← b
    *b = *c     // b ← c
    *c = temp   // c ← 原来的 a
}

func main() {
    x, y, z := 1, 2, 3
    fmt.Printf("交换前: x=%d, y=%d, z=%d\n", x, y, z)

    rotate3(&x, &y, &z)
    fmt.Printf("交换后: x=%d, y=%d, z=%d\n", x, y, z)
    // 交换后: x=2, y=3, z=1

    // 再执行一次
    rotate3(&x, &y, &z)
    fmt.Printf("再次交换: x=%d, y=%d, z=%d\n", x, y, z)
    // 再次交换: x=3, y=1, z=2
}

关键点:利用临时变量存储需要覆盖的值,然后依次赋值,完成三变量的循环左移。

练习 3:new() 与值传递

以下代码的输出是什么?解释 new() 和值传递的行为。

package main

import "fmt"

func modify(p *int) {
    p = new(int)  // p 指向了新的内存
    *p = 100
}

func main() {
    x := 42
    modify(&x)
    fmt.Println(x)
}
参考答案

输出

42

分析

  1. x := 42 → x 存储值 42
  2. modify(&x) → 将 x 的地址传递给 modify 函数
  3. modify 函数内部:
    • 参数 p 接收到的是 x 地址的副本
    • p = new(int) → 让局部的 p 指向新的内存地址(x 的地址副本被覆盖了)
    • *p = 100 → 修改的是新分配的内存,不是 x
  4. 函数返回后,x 的值没有变化

关键知识点:指针参数本身也是值传递(传递地址的副本)。如果在函数内重新赋值指针(p = new(int)),只会修改副本的指向,不会影响调用者的原始指针。要修改原始值,应该使用 *p = 100 而不是 p = new(int)

修正后的代码

func modify(p *int) {
    *p = 100  // 通过指针修改原始值
}

func main() {
    x := 42
    modify(&x)
    fmt.Println(x)  // 100
}

搜索