Go 从 0 到精通 · 第 07 课:指针入门
Go 从 0 到精通 · 第 07 课:指针入门
学习定位:这是整套 Go 教程的第 7 课,也是阶段二(核心语法阶段)的第一课。
前置要求:已经完成第 6 课,掌握了函数的定义、调用、参数传递(值传递)、多返回值、可变参数,理解了"函数内部修改参数不影响外部变量"这一事实。
本课目标:理解指针的概念与使用方式,掌握取地址&和解引用*,理解指针如何解决"在函数内修改外部变量"的问题,建立对指针安全边界的认知。
1. 本课你要解决的核心问题
第 6 课结尾留下了一个悬念:
Go 是值传递,函数内部修改参数不影响外部变量。那如果我就是需要在函数里修改外部变量,怎么办?
答案就是指针。
你需要搞明白以下问题:
- 什么是指针,它存的是什么
&和*分别做什么- 指针怎么让函数修改外部变量
- 指针的零值是什么,有什么用
- Go 的指针和 C 的指针有什么区别(更安全)
- 哪些场景该用指针,哪些不该用
学完这一课,你就能理解 fmt.Scan 里为什么要写 &input 了。
2. 从值传递的问题出发
2.1 回顾:值传递改不了外部变量
1 | package main |
输出:
1 | 函数内部 n = 10 |
double 拿到的是 x 的一份副本,修改的是副本,x 本身没有变。
2.2 现实中的类比
想象你有一个文件柜(变量 x),里面放着一份文件(值 5)。
值传递就像:你把文件复印了一份,交给别人(函数)。别人在复印件上涂涂改改,原件根本不知道,也没有变。
如果你想让别人直接改原件,你需要告诉他原件在哪个柜子的哪个抽屉——也就是告诉他文件的地址。
指针就是变量的地址。
3. 什么是指针
3.1 指针的本质
每个变量在内存中都有一个位置,这个位置有一个编号,叫做内存地址。
指针就是一个变量,它存的不是普通的数据(比如 5、"hello"),而是另一个变量的内存地址。
打个比方:
- 普通变量:存的是"值",比如
5 - 指针变量:存的是"地址",比如"变量
x住在内存的 0xc000018090 这个位置"
3.2 用图来理解
1 | 普通变量 x: |
p 说:“我不存数据本身,我告诉你数据在哪。”
4. 取地址 & 与解引用 *
4.1 &:取地址
&变量名 表示"获取这个变量的地址":
1 | package main |
输出(地址每次运行可能不同):
1 | x 的值: 42 |
&x 就是"变量 x 在内存中的地址"。
4.2 声明指针变量
指针变量的类型写法是 *类型,表示"指向某类型的指针":
1 | package main |
输出:
1 | x = 42 |
p的类型是*int(指向int的指针)p的值是&x(x的地址)
4.3 *:解引用(通过指针访问值)
*指针变量 表示"通过地址找到那个变量,拿到它的值":
1 | package main |
输出:
1 | x = 42 |
*p 就是"顺着 p 存的地址,找到那个变量,取出它的值"。
4.4 通过指针修改值
这是指针最重要的用法——通过指针修改原变量:
1 | package main |
输出:
1 | x = 100 |
*p = 100 的含义:顺着 p 存的地址(也就是 x 的地址),把那个位置的值改成 100。
所以 x 被修改了。
5. 指针与函数参数:解决第 6 课的悬念
5.1 用指针在函数里修改外部变量
1 | package main |
输出:
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 | var input string |
fmt.Scanln 需要把用户输入写到 input 里。因为 Go 是值传递,如果不传指针,Scanln 只能修改 input 的副本,input 本身不会变。
传 &input 就是告诉 Scanln:input 在这个地址,你直接往这里写。
6. 指针的零值:nil
6.1 什么是 nil
指针的零值是 nil,表示"不指向任何东西"。
1 | package main |
输出:
1 | p = <nil> |
6.2 nil 指针不能解引用
1 | var p *int // p 是 nil |
对 nil 指针做解引用会导致程序崩溃(panic)。这是指针最常见的错误之一。
在使用指针前,应该先判断它是否为 nil:
1 | if p != nil { |
7. Go 指针 vs C 指针:更安全
如果你有 C/C++ 的背景,可能会对指针有些心理阴影。Go 的指针做了大量简化,安全性远高于 C。
7.1 Go 没有指针运算
C 中可以对指针做加减运算(p++、p + 3),这非常强大但也非常危险。
Go 不允许指针运算:
1 | p := &x |
这从根本上消除了"指针越界"这类经典 bug。
7.2 Go 有垃圾回收
C 中你需要手动 malloc 和 free,忘记释放就内存泄漏,释放后还用就段错误。
Go 有垃圾回收(GC),你不需要手动管理指针指向的内存。变量不再被引用时,GC 会自动回收。
7.3 总结对比
| 特性 | C 指针 | Go 指针 |
|---|---|---|
| 指针运算 | 支持 | 不支持 |
| 手动内存管理 | 需要 | 不需要(GC) |
| 危险程度 | 高 | 低 |
| 学习曲线 | 陡峭 | 平缓 |
Go 的指针保留了"通过地址间接访问数据"的核心能力,但去掉了最容易出错的部分。你只需要理解 & 和 * 就够用了。
8. 结构体与指针(初步接触)
这一课先简单看一下指针和结构体的配合,结构体的详细内容在第 12 课。
1 | package main |
输出:
1 | 小明 现在 21 岁 |
birthday 接收一个 *Person,通过指针直接修改了 person 的 Age 字段。
注意:Go 对结构体指针访问字段做了语法糖——p.Age 实际上等价于 (*p).Age,你不需要写那对括号。
9. 什么时候用指针,什么时候不用
这是实践中最重要的判断之一。
9.1 需要用指针的场景
- 需要在函数里修改外部变量:这是最直接的理由
- 避免大结构体的复制开销:结构体很大时,值传递会复制整个结构体,传指针只复制一个地址(8 字节)
- 表示"可选"语义:指针可以是
nil,表示"没有值"
9.2 不需要用指针的场景
- 小类型(
int、float64、bool、string):复制成本极低,值传递更简洁 - 不需要修改原值:如果函数只是读取数据,值传递就够了
nil状态不是你想要的:值类型没有nil,减少空指针检查
9.3 实践经验
Go 社区的经验法则:如果不确定,先用值传递。遇到性能问题或需要修改原值时,再改用指针。
不要一开始就到处传指针——过度使用指针反而增加代码复杂度和 nil 检查负担。
10. 一段综合示例
1 | package main |
输出:
1 | 交换前:x=10, y=20 |
11. 常见坑总结
11.1 对 nil 指针解引用
1 | var p *int |
防范:使用指针前先判断 p != nil。
11.2 混淆 * 的两种含义
* 出现在两个地方,含义不同:
1 | // 1. 类型中的 *:表示"指向某类型的指针" |
区分方法:看上下文。* 在类型位置是"指针类型",在值位置是"解引用"。
11.3 以为 & 能取任何东西的地址
1 | &42 // 编译错误:不能取字面量的地址 |
& 只能取变量的地址,不能取字面量或临时表达式的地址。
11.4 返回局部变量的地址
在 Go 中,返回局部变量的地址是安全的:
1 | func newValue() *int { |
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),交换a和b指向的值 - 在
main中测试,验证交换效果
练习 3:安全解引用
要求:
- 定义函数
safeDeref(p *int) int,如果p是nil返回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 概念题
- 什么是指针?指针变量存的是什么?
&做什么?*在表达式中做什么?*int和int有什么区别?- 指针的零值是什么?
- 对
nil指针解引用会发生什么? - Go 的指针和 C 的指针有什么核心区别?
- 为什么 Go 中返回局部变量的地址是安全的?
- 什么时候应该用指针?什么时候不该用?
fmt.Scanln(&input)中的&是什么意思?p.Age和(*p).Age有什么关系?
如果你能流畅回答这些问题,说明指针的基本概念你已经掌握了。
14. 本课总结
这一课你学到了 Go 中最核心的间接访问机制——指针。
你现在应该已经理解:
- 指针是存地址的变量,通过
&取地址,通过*解引用 - 指针让函数能修改外部变量,解决了值传递的局限
*int是"指向int的指针"类型,*p是"解引用p,取它指向的值"- 指针的零值是
nil,对nil解引用会 panic - Go 的指针比 C 安全:没有指针运算,有垃圾回收
- 返回局部变量的地址在 Go 中是安全的(逃逸分析)
- 不要过度使用指针——需要修改原值或避免大对象复制时才用
15. 下一课预告
下一课我们学习:数组。
会重点讲:
- 数组的声明和初始化
- 数组的长度是类型的一部分
- 数组的遍历
- 数组是值类型——赋值和传参都会复制整个数组
- 为什么 Go 中更常用切片而不是数组
学完下一课,你就能理解为什么说"Go 的数组是固定长度的",以及为什么切片才是日常开发中的主角。





