Go 零基础教程

Go 指针入门

指针(Pointers)是 Go 语言中一个极为硬核且基础的概念,它允许你直接与计算机的内存地址打交道。

熟练掌握指针,是你写出高性能、低内存占用 Go 代码的必经之路。特别是在处理复杂数据结构(如大型 Struct)、函数参数传递以及底层内存管理时,指针的作用无可替代。

本章将带你从零开始,搞懂如何声明指针、如何通过解引用来读取和修改真实数据,以及指针与普通变量之间那种“千丝万缕”的联系。

1. 什么是指针?

指针也是一种变量,只不过它存储的不是普通的数值(比如 10 或者 "Hello"),而是另一个变量的“内存地址”。

打个通俗的比方:如果把计算机内存看作一条长长的街道,普通的变量就像是街道上的一栋栋房子,里面装着具体的人(数据)。而指针,就像是写着某栋房子具体门牌号的纸条。指针本身不装人,它只告诉你:“顺着这个门牌号找过去,你就能找到那个人”。

1.1 声明指针

在 Go 语言中声明指针,你需要使用星号 * 符号,紧跟在它将要指向的数据类型前面。基本语法如下:

var 指针名 *数据类型

例如,要声明一个专门用来存放整数 (int) 内存地址的指针,你可以这样写:

var ptr *int

这行代码声明了一个名为 ptr 的变量。需要牢记的是,ptr 自己也要占用一点点内存空间,而它里面装的内容,将是一个内存地址。

1.2 基础示例

来看一个完整的例子,感受一下指针是如何和普通变量绑定的:

package main

import "fmt"

func main() {
	var num int = 42
	var ptr *int
	
	// 把变量 'num' 的内存地址提取出来,赋值给 'ptr'
	ptr = &num 
	
	fmt.Println("num 的内存地址:", &num) // 输出类似: 0xc0000160b8 (每次运行可能不同)
	fmt.Println("ptr 里面存的值:", ptr)  // 输出类似: 0xc0000160b8 (与上面的地址完全一样!)
}

在这个例子中:

  • num 是一个普通整数,值为 42。
  • ptr 是一个指针变量。
  • &num 使用了取址符 &,它的作用是揪出 num 变量在内存中的真实物理地址。
  • ptr = &num 就是把这张写着 num 地址的“门牌号纸条”交给了 ptr 保存。

2. 指针的零值与防雷指南

和 Go 语言中的其他类型一样,指针也有自己的默认“零值”。指针的零值是 nil

一个 nil 指针意味着这张“门牌号纸条”是空白的,它没有指向任何现实存在的内存地址。这是一个极其危险的雷区:如果你试图强行访问(解引用)一个 nil 指针指向的数据,程序会直接崩溃 (Panic)!

package main

import "fmt"

func main() {
	var ptr *int // 声明了,但没给它任何地址,所以它是 nil
	
	fmt.Println("ptr 的值:", ptr)   // 输出: <nil>
	fmt.Println("ptr == nil:", ptr == nil) // 输出: true
	
	// 🚨 致命错误演示:取消下面这行的注释会导致程序崩溃
	// fmt.Println(*ptr) // Panic: invalid memory address or nil pointer dereference
}

黄金法则: 在顺着指针去拿数据之前,一定要确保它不是空的(if ptr != nil)。

3. 取址符 (&):获取内存地址

如前所述,取址符 & 是我们连接普通变量和指针的桥梁。把它放在任何一个普通的变量前面,你就能得到它在内存中的安身之所。

package main

import "fmt"

func main() {
	name := "Alice"
	address := &name // 提取 'name' 变量的内存地址
	
	fmt.Println("name 的值:", name)       // 输出: Alice
	fmt.Println("name 的地址:", &name)   // 输出类似: 0xc000010230
	fmt.Println("address 变量的值:", address) // 输出类似: 0xc000010230
}

4. 解引用 (*):通过地址读写数据

既然我们有了地址,下一步自然是顺着地址去找数据了。这个过程被称为解引用 (Dereferencing)

在指针变量的前面加上星号 *,就等于下达了指令:“Go,顺着这个地址找过去,把里面的数据给我读出来,或者让我改掉它!”

4.1 读取真实数据

package main

import "fmt"

func main() {
	num := 100
	ptr := &num // 'ptr' 拿到了 'num' 的地址
	
	fmt.Println("num 本身的值:", num)   // 输出: 100
	fmt.Println("ptr 存的地址:", ptr)   // 输出类似: 0xc000010248
	
	// 使用 * 解引用:顺着 ptr 存的地址,抓取里面的数据
	fmt.Println("ptr 指向的真实数据:", *ptr)  // 输出: 100
}

4.2 隔山打牛:通过指针修改原数据

这是指针最强大的威力所在。当你解引用一个指针并对其进行赋值时,你实际上是直接在修改那个原始变量的内存数据

package main

import "fmt"

// modifyValue 函数接收一个整数的指针作为参数
func modifyValue(ptr *int) {
    // 顺着指针找到原数据,将其翻倍,再存回那个内存地址
	*ptr = *ptr * 2 
}

func main() {
	number := 50
	fmt.Println("修改前的值:", number) // 输出: 50
	
	// 注意:我们要传的是 number 的地址,所以要加 &
	modifyValue(&number) 
	
	// 原来的 number 已经被隔空修改了!
	fmt.Println("修改后的值:", number) // 输出: 100
}

在这个例子中,因为我们把 number 的真实地址(&number)交给了 modifyValue 函数,函数内部通过 *ptr 直接操作了那块内存,从而成功地修改了外面的 number 变量。这打破了常规函数“传值拷贝”的隔离。

5. 指针与结构体 (Struct) 的梦幻联动

在实际开发中,指针最常用于和复杂的数据结构(尤其是结构体 Struct)配合使用。传递大型结构体的指针,不仅能让你修改里面的字段,还能避免在函数间传递时发生极其消耗性能的全量内存拷贝。

package main

import "fmt"

type Person struct {
	Name string
	Age  int
}

func main() {
	person := Person{Name: "Bob", Age: 30}
	ptr := &person // 拿到 person 结构体的指针
	
	fmt.Println("原始的 Person:", person) // 输出: {Bob 30}
	
	// 严格语法的解引用读取字段
	fmt.Println("通过指针读取名字:", (*ptr).Name) // 输出: Bob
	
	// 🌟 Go 语言的贴心语法糖:自动隐式解引用
	// 你不需要写 (*ptr).Age = 35,直接写 ptr.Age 即可,Go 编译器会自动帮你解引用!
	ptr.Age = 35 
	
	fmt.Println("修改后的 Person:", person) // 输出: {Bob 35}
}

注意上面的代码:理论上你需要写 (*ptr).Name 来确保先解引用拿到结构体,再点出 Name 字段(因为 . 运算符的优先级高于 *)。但 Go 语言非常人性化,当通过结构体指针访问字段时,允许你直接简写为 ptr.Age,编译器会在底层自动帮你完成解引用。