# 第 09 课:指针 ## 学习目标 学会区分变量的值、变量的地址和指针变量,理解怎样通过函数修改调用方的数据,以及测试参数 `t *testing.T` 中星号的含义。 按 [main.go](./main.go) 的 1~7 节逐段手敲。本课只使用普通指针和结构体,不涉及二级指针、unsafe 或复杂内存管理。共享根目录的 `go.mod`,无需第三方依赖。 ## 1. 三个写法,分别看待 ```go number := 10 var pointer *int = &number fmt.Println(*pointer) ``` | 写法 | 含义 | | --- | --- | | `number` | 一个整数变量,当前值为 10 | | `&number` | 取得 number 的地址 | | `*int` | 类型:指向 int 的指针 | | `pointer` | 保存地址的指针变量 | | `*pointer` | 表达式:访问 pointer 指向的变量,称为解引用 | 星号的位置不同,含义不同:声明中的 `*int` 是类型,表达式中的 `*pointer` 是访问目标。`*pointer = 20` 修改目标整数;`pointer = &other` 则让指针改为指向另一个整数。 可以用 `fmt.Printf("地址:%p\n", pointer)` 观察地址。具体地址不固定,不要背诵某次输出,也不要依赖两次运行地址相同。示例用地址比较保证默认输出稳定。 ## 2. 函数参数仍然按值传递 普通参数: ```go func changeValue(value int) { value = 100 } ``` 调用 `changeValue(number)` 只修改整数副本,不影响 number。 指针参数: ```go func changeByPointer(value *int) bool { if value == nil { return false } *value = 100 return true } ``` 调用 `changeByPointer(&number)` 时,把地址值复制给参数 value。value 和调用方都能找到同一个整数,因此通过 `*value` 可以修改 number。 **Go 仍然是按值传递,不是出现指针后就变成另一种参数传递规则。** 复制的是地址值,而不是目标整数。 另外,在函数内写 `value = &local` 只改变参数指针的副本,不会改变调用方指针本身。示例中的 redirect 专门演示这个区别。本课不需要掌握怎样修改调用方指针的指向。 ## 3. nil 指针 ```go var empty *int ``` 指针零值是 nil,表示没有指向有效变量。可以判断 `empty == nil`,但不能执行 `*empty`,否则会发生运行时 panic。 指针参数是否允许 nil,取决于函数约定。本课函数主动检查,遇到 nil 返回 false;不能因此假设所有库函数都接受 nil。 `var empty *int` 不会自动创建一个目标整数。与之不同: ```go created := new(int) ``` 会创建一个零值 int 并返回它的指针,`created != nil`,`*created == 0`。通常 `number := 0; pointer := &number` 也能满足类似需求,不必强制使用 new。 ## 4. 结构体指针 ```go func birthday(student *Student) bool { if student == nil { return false } student.Age++ return true } ``` 调用 `birthday(&student)`,修改的是原学生的年龄。 Go 允许直接写 `student.Age`,这里等价于 `(*student).Age`。Go 没有 C 中的 `->` 运算符;nil 结构体指针也不能直接访问字段。 ## 5. 再看测试参数 ```go func TestAdd(t *testing.T) { // t.Fatalf(...) 等操作由 testing 包提供。 } ``` 现在可以拆开理解: - t:参数变量名。 - testing.T:testing 包中提供的测试管理类型。 - `*testing.T`:指向这个类型的指针。 - 测试框架调用函数时传入 t,用户不需要自己创建或调用 TestAdd。 `t.Fatalf` 不是结构体里的普通数据字段,而是 testing.T 提供的方法,下一课学习“方法”时再深入理解。本课不需要手动创建 testing.T。 ## 6. 与 C 相比 - 都用 `&` 取地址,用 `*` 声明指针类型或解引用。 - Go 的指针不支持普通 C 风格的 `p++`、`p + 1` 指针算术。 - 普通 Go 代码不需要手动 malloc / free,内存由运行时垃圾回收管理;文件等系统资源仍需要适当关闭。 - 可以返回局部变量的地址,Go 会保证仍被使用的数据保持有效;不要直接套用 C 的栈上悬空指针规则。具体分配在栈还是堆由编译器分析,不由 `&` 或 new 单独决定。 - 指针能修改共享变量,但不自动提供并发安全,后续并发课程再讨论同步。 ## 编译并运行 在**仓库根目录**执行: ```powershell New-Item -ItemType Directory -Force bin/09_pointers | Out-Null go build -o ./bin/09_pointers/pointers.exe ./lessons/09_pointers .\bin\09_pointers\pointers.exe ``` 修改源码后需要重新编译。快速编译并运行: ```powershell go run ./lessons/09_pointers ``` 或者从根目录进入本课: ```powershell Set-Location lessons/09_pointers go run . ``` ## 预期输出 ```text === 取地址与解引用 === number 的值:10 指针类型:*int,是否指向 number:true 通过 *pointer 读取:10 通过指针修改后,number:20 === 函数修改原变量 === 普通参数调用后:20 指针参数调用后:100,成功:true === 指针复制与重新指向 === 通过 alias 修改后,number:30 函数内重新指向后,原指针仍指向 number:true === nil 指针 === 空指针为 nil:true 传入 nil,修改成功:false === 结构体指针 === 小朱 过生日后,年龄:21 === new 创建指针 === 指针为 nil:false,初始值:0 修改后的值:42 ``` ## 动手练习 - [ ] 用 `%p` 打印 `&number` 和 pointer,确认它们相等。 - [ ] 创建另一个整数 other,让 pointer 指向它,观察 `*pointer = 50` 修改了谁。 - [ ] 给 birthday 传入 nil,检查返回结果。 - [ ] 新写 `reset(value *int) bool`,检查 nil 后把目标整数设为 0。 - [ ] 对比 `var p *int` 与 `p := new(int)` 的 nil 状态和是否能解引用。 - [ ] 回看第七课的 `t *testing.T`,区分参数名、包名和类型。 如果试验 nil 解引用,一次只保留这一处故意错误,观察 panic 后恢复注释。若另行手敲,放到本课的 `practice/` 子目录,用 `go run ./lessons/09_pointers/practice` 运行,不要在同一个包中重复定义 Student 或 main。 下一课学习方法:理解接收者、值接收者与指针接收者,让结构体拥有自己的操作。 [上一课:结构体](../08_structs/README.md) · [学习总览](../../README.md)