Go 从 0 到精通 · 第 07 课:指针入门

学习定位:这是整套 Go 教程的第 7 课,也是阶段二(核心语法阶段)的第一课。
前置要求:已经完成第 6 课,掌握了函数的定义、调用、参数传递(值传递)、多返回值、可变参数,理解了"函数内部修改参数不影响外部变量"这一事实。
本课目标:理解指针的概念与使用方式,掌握取地址 & 和解引用 *,理解指针如何解决"在函数内修改外部变量"的问题,建立对指针安全边界的认知。


1. 本课你要解决的核心问题

第 6 课结尾留下了一个悬念:

Go 是值传递,函数内部修改参数不影响外部变量。那如果我就是需要在函数里修改外部变量,怎么办?

答案就是指针

你需要搞明白以下问题:

  • 什么是指针,它存的是什么
  • &* 分别做什么
  • 指针怎么让函数修改外部变量
  • 指针的零值是什么,有什么用
  • Go 的指针和 C 的指针有什么区别(更安全)
  • 哪些场景该用指针,哪些不该用

学完这一课,你就能理解 fmt.Scan 里为什么要写 &input 了。


2. 从值传递的问题出发

2.1 回顾:值传递改不了外部变量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package main

import "fmt"

func double(n int) {
n = n * 2
fmt.Println("函数内部 n =", n)
}

func main() {
x := 5
double(x)
fmt.Println("函数外部 x =", x)
}

输出:

1
2
函数内部 n = 10
函数外部 x = 5

double 拿到的是 x 的一份副本,修改的是副本,x 本身没有变。

2.2 现实中的类比

想象你有一个文件柜(变量 x),里面放着一份文件(值 5)。

值传递就像:你把文件复印了一份,交给别人(函数)。别人在复印件上涂涂改改,原件根本不知道,也没有变。

如果你想让别人直接改原件,你需要告诉他原件在哪个柜子的哪个抽屉——也就是告诉他文件的地址

指针就是变量的地址。


3. 什么是指针

3.1 指针的本质

每个变量在内存中都有一个位置,这个位置有一个编号,叫做内存地址

指针就是一个变量,它存的不是普通的数据(比如 5"hello"),而是另一个变量的内存地址

打个比方:

  • 普通变量:存的是"值",比如 5
  • 指针变量:存的是"地址",比如"变量 x 住在内存的 0xc000018090 这个位置"

3.2 用图来理解

1
2
3
4
5
6
7
8
9
普通变量 x:
变量名: x
: 5
地址: 0xc000018090

指针变量 p:
变量名: p
: 0xc000018090 ← 这是 x 的地址
地址: 0xc0000180a0 ← p 自己也有地址

p 说:“我不存数据本身,我告诉你数据在哪。”


4. 取地址 & 与解引用 *

4.1 &:取地址

&变量名 表示"获取这个变量的地址":

1
2
3
4
5
6
7
8
9
package main

import "fmt"

func main() {
x := 42
fmt.Println("x 的值:", x)
fmt.Println("x 的地址:", &x)
}

输出(地址每次运行可能不同):

1
2
x 的值: 42
x 的地址: 0xc000018090

&x 就是"变量 x 在内存中的地址"。

4.2 声明指针变量

指针变量的类型写法是 *类型,表示"指向某类型的指针":

1
2
3
4
5
6
7
8
9
10
11
12
package main

import "fmt"

func main() {
x := 42
p := &x // p 的类型是 *int,即"指向 int 的指针"

fmt.Println("x =", x)
fmt.Println("p =", p) // p 存的是 x 的地址
fmt.Printf("p 的类型:%T\n", p)
}

输出:

1
2
3
x = 42
p = 0xc000018090
p 的类型:*int
  • p 的类型是 *int(指向 int 的指针)
  • p 的值是 &xx 的地址)

4.3 *:解引用(通过指针访问值)

*指针变量 表示"通过地址找到那个变量,拿到它的值":

1
2
3
4
5
6
7
8
9
10
11
package main

import "fmt"

func main() {
x := 42
p := &x

fmt.Println("x =", x)
fmt.Println("*p =", *p) // 通过 p 找到 x,取出 x 的值
}

输出:

1
2
x = 42
*p = 42

*p 就是"顺着 p 存的地址,找到那个变量,取出它的值"。

4.4 通过指针修改值

这是指针最重要的用法——通过指针修改原变量

1
2
3
4
5
6
7
8
9
10
11
12
package main

import "fmt"

func main() {
x := 42
p := &x

*p = 100 // 通过 p 找到 x,把 x 的值改成 100

fmt.Println("x =", x)
}

输出:

1
x = 100

*p = 100 的含义:顺着 p 存的地址(也就是 x 的地址),把那个位置的值改成 100

所以 x 被修改了。


5. 指针与函数参数:解决第 6 课的悬念

5.1 用指针在函数里修改外部变量

1
2
3
4
5
6
7
8
9
10
11
12
13
package main

import "fmt"

func double(n *int) {
*n = *n * 2
}

func main() {
x := 5
double(&x) // 传入 x 的地址
fmt.Println("x =", x)
}

输出:

1
x = 10

对比一下值传递版本(第 6 课)和指针版本:

方式 函数签名 调用 效果
值传递 func double(n int) double(x) x 不变
指针传递 func double(n *int) double(&x) x 被修改

关键区别:

  • 值传递:函数拿到的是 x副本
  • 指针传递:函数拿到的是 x地址,通过地址直接操作 x 本身

5.2 回答第 6 课的问题:fmt.Scan 为什么需要 &

1
2
var input string
fmt.Scanln(&input) // 为什么加 &?

fmt.Scanln 需要把用户输入写到 input 里。因为 Go 是值传递,如果不传指针,Scanln 只能修改 input 的副本,input 本身不会变。

&input 就是告诉 Scanlninput 在这个地址,你直接往这里写。


6. 指针的零值:nil

6.1 什么是 nil

指针的零值是 nil,表示"不指向任何东西"。

1
2
3
4
5
6
7
8
9
package main

import "fmt"

func main() {
var p *int // 声明但不赋值,p 的零值是 nil
fmt.Println("p =", p)
fmt.Println("p == nil:", p == nil)
}

输出:

1
2
p = <nil>
p == nil: true

6.2 nil 指针不能解引用

1
2
var p *int       // p 是 nil
fmt.Println(*p) // 运行时 panic!

nil 指针做解引用会导致程序崩溃(panic)。这是指针最常见的错误之一。

在使用指针前,应该先判断它是否为 nil

1
2
3
if p != nil {
fmt.Println(*p)
}

7. Go 指针 vs C 指针:更安全

如果你有 C/C++ 的背景,可能会对指针有些心理阴影。Go 的指针做了大量简化,安全性远高于 C。

7.1 Go 没有指针运算

C 中可以对指针做加减运算(p++p + 3),这非常强大但也非常危险。

Go 不允许指针运算:

1
2
p := &x
p++ // 编译错误

这从根本上消除了"指针越界"这类经典 bug。

7.2 Go 有垃圾回收

C 中你需要手动 mallocfree,忘记释放就内存泄漏,释放后还用就段错误。

Go 有垃圾回收(GC),你不需要手动管理指针指向的内存。变量不再被引用时,GC 会自动回收。

7.3 总结对比

特性 C 指针 Go 指针
指针运算 支持 不支持
手动内存管理 需要 不需要(GC)
危险程度
学习曲线 陡峭 平缓

Go 的指针保留了"通过地址间接访问数据"的核心能力,但去掉了最容易出错的部分。你只需要理解 &* 就够用了。


8. 结构体与指针(初步接触)

这一课先简单看一下指针和结构体的配合,结构体的详细内容在第 12 课。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package main

import "fmt"

type Person struct {
Name string
Age int
}

func birthday(p *Person) {
p.Age++
}

func main() {
person := Person{Name: "小明", Age: 20}
birthday(&person)
fmt.Printf("%s 现在 %d 岁\n", person.Name, person.Age)
}

输出:

1
小明 现在 21 

birthday 接收一个 *Person,通过指针直接修改了 personAge 字段。

注意:Go 对结构体指针访问字段做了语法糖——p.Age 实际上等价于 (*p).Age,你不需要写那对括号。


9. 什么时候用指针,什么时候不用

这是实践中最重要的判断之一。

9.1 需要用指针的场景

  • 需要在函数里修改外部变量:这是最直接的理由
  • 避免大结构体的复制开销:结构体很大时,值传递会复制整个结构体,传指针只复制一个地址(8 字节)
  • 表示"可选"语义:指针可以是 nil,表示"没有值"

9.2 不需要用指针的场景

  • 小类型intfloat64boolstring):复制成本极低,值传递更简洁
  • 不需要修改原值:如果函数只是读取数据,值传递就够了
  • nil 状态不是你想要的:值类型没有 nil,减少空指针检查

9.3 实践经验

Go 社区的经验法则:如果不确定,先用值传递。遇到性能问题或需要修改原值时,再改用指针。

不要一开始就到处传指针——过度使用指针反而增加代码复杂度和 nil 检查负担。


10. 一段综合示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
package main

import "fmt"

// 用指针交换两个变量的值
func swap(a, b *int) {
*a, *b = *b, *a
}

// 用指针累加
func increment(n *int) {
*n++
}

// 用指针设置值(如果指针非 nil)
func setValue(p *int, val int) {
if p != nil {
*p = val
}
}

func main() {
// 交换
x, y := 10, 20
fmt.Printf("交换前:x=%d, y=%d\n", x, y)
swap(&x, &y)
fmt.Printf("交换后:x=%d, y=%d\n", x, y)

// 累加
count := 0
increment(&count)
increment(&count)
increment(&count)
fmt.Println("count =", count)

// 安全设置值
var p *int
setValue(p, 100) // p 是 nil,不会 panic

var z int
setValue(&z, 42)
fmt.Println("z =", z)

// nil 检查
var q *int
if q == nil {
fmt.Println("q 是 nil,不能解引用")
}
}

输出:

1
2
3
4
5
交换前:x=10, y=20
交换后:x=20, y=10
count = 3
z = 42
q 是 nil,不能解引用

11. 常见坑总结

11.1 对 nil 指针解引用

1
2
var p *int
fmt.Println(*p) // panic: runtime error

防范:使用指针前先判断 p != nil

11.2 混淆 * 的两种含义

* 出现在两个地方,含义不同:

1
2
3
4
5
// 1. 类型中的 *:表示"指向某类型的指针"
var p *int

// 2. 表达式中的 *:表示"解引用,取指针指向的值"
val := *p

区分方法:看上下文。* 在类型位置是"指针类型",在值位置是"解引用"。

11.3 以为 & 能取任何东西的地址

1
2
&42        // 编译错误:不能取字面量的地址
&(x + 1) // 编译错误:不能取表达式的地址

& 只能取变量的地址,不能取字面量或临时表达式的地址。

11.4 返回局部变量的地址

在 Go 中,返回局部变量的地址是安全的

1
2
3
4
func newValue() *int {
x := 42
return &x // 安全!Go 编译器会把 x 分配到堆上
}

Go 的逃逸分析会自动检测这种情况,把 x 从栈上移到堆上,确保返回的地址有效。

这和 C 不同——C 返回局部变量的地址是危险的(悬空指针),Go 完全没有这个问题。

11.5 到处传指针增加复杂度

新手学会指针后,容易到处传指针。但过度使用指针会:

  • 增加 nil 检查负担
  • 让函数的副作用更难追踪
  • 代码可读性下降

原则:需要修改原值或避免大对象复制时才用指针。


12. 本课练习

练习 1:用指针实现翻倍

要求:

  • 定义函数 double(n *int),将 n 指向的值翻倍
  • main 中定义 x := 7,调用 double(&x),打印 x
  • 验证 x 是否被修改

练习 2:用指针交换两个变量

要求:

  • 定义函数 swap(a, b *int),交换 ab 指向的值
  • main 中测试,验证交换效果

练习 3:安全解引用

要求:

  • 定义函数 safeDeref(p *int) int,如果 pnil 返回 0,否则返回 *p
  • main 中分别传入 nil 和非 nil 指针测试

练习 4:用指针统计调用次数

要求:

  • 定义函数 countCall(counter *int),每次调用把 *counter 加 1
  • main 中多次调用,打印计数器的值

练习 5:理解值传递与指针传递的区别

要求:

  • 写两个函数:addTen(n int)addTenByPointer(n *int)
  • 前者尝试给参数加 10,后者通过指针加 10
  • main 中分别调用,打印结果
  • 自己解释为什么结果不同

练习 6:用指针返回多个结果

要求:

  • 定义函数 minMax(a, b int, min, max *int)
  • 在函数内部把较小值写入 *min,较大值写入 *max
  • main 中调用并打印结果
  • 对比第 6 课用多返回值实现同样功能的写法,讨论哪种更好

13. 自测题

13.1 概念题

  1. 什么是指针?指针变量存的是什么?
  2. & 做什么?* 在表达式中做什么?
  3. *intint 有什么区别?
  4. 指针的零值是什么?
  5. nil 指针解引用会发生什么?
  6. Go 的指针和 C 的指针有什么核心区别?
  7. 为什么 Go 中返回局部变量的地址是安全的?
  8. 什么时候应该用指针?什么时候不该用?
  9. fmt.Scanln(&input) 中的 & 是什么意思?
  10. p.Age(*p).Age 有什么关系?

如果你能流畅回答这些问题,说明指针的基本概念你已经掌握了。


14. 本课总结

这一课你学到了 Go 中最核心的间接访问机制——指针。

你现在应该已经理解:

  • 指针是存地址的变量,通过 & 取地址,通过 * 解引用
  • 指针让函数能修改外部变量,解决了值传递的局限
  • *int 是"指向 int 的指针"类型,*p 是"解引用 p,取它指向的值"
  • 指针的零值是 nil,对 nil 解引用会 panic
  • Go 的指针比 C 安全:没有指针运算,有垃圾回收
  • 返回局部变量的地址在 Go 中是安全的(逃逸分析)
  • 不要过度使用指针——需要修改原值或避免大对象复制时才用

15. 下一课预告

下一课我们学习:数组

会重点讲:

  • 数组的声明和初始化
  • 数组的长度是类型的一部分
  • 数组的遍历
  • 数组是值类型——赋值和传参都会复制整个数组
  • 为什么 Go 中更常用切片而不是数组

学完下一课,你就能理解为什么说"Go 的数组是固定长度的",以及为什么切片才是日常开发中的主角。