README.md 6.2 KB

第 09 课:指针

学习目标

学会区分变量的值、变量的地址和指针变量,理解怎样通过函数修改调用方的数据,以及测试参数 t *testing.T 中星号的含义。

按 main.go 的 1~7 节逐段手敲。本课只使用普通指针和结构体,不涉及二级指针、unsafe 或复杂内存管理。共享根目录的 go.mod,无需第三方依赖。

1. 三个写法,分别看待

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. 函数参数仍然按值传递

普通参数:

func changeValue(value int) {
    value = 100
}

调用 changeValue(number) 只修改整数副本,不影响 number。

指针参数:

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 指针

var empty *int

指针零值是 nil,表示没有指向有效变量。可以判断 empty == nil,但不能执行 *empty,否则会发生运行时 panic。

指针参数是否允许 nil,取决于函数约定。本课函数主动检查,遇到 nil 返回 false;不能因此假设所有库函数都接受 nil。

var empty *int 不会自动创建一个目标整数。与之不同:

created := new(int)

会创建一个零值 int 并返回它的指针,created != nil,*created == 0。通常 number := 0; pointer := &number 也能满足类似需求,不必强制使用 new。

4. 结构体指针

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. 再看测试参数

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 单独决定。
  • 指针能修改共享变量,但不自动提供并发安全,后续并发课程再讨论同步。

编译并运行

在仓库根目录执行:

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

修改源码后需要重新编译。快速编译并运行:

go run ./lessons/09_pointers

或者从根目录进入本课:

Set-Location lessons/09_pointers
go run .

预期输出

=== 取地址与解引用 ===
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。

下一课学习方法:理解接收者、值接收者与指针接收者,让结构体拥有自己的操作。

上一课:结构体 · 学习总览