|
|
@@ -0,0 +1,201 @@
|
|
|
+# 第 16 课:接口 interface
|
|
|
+
|
|
|
+## 学习目标
|
|
|
+
|
|
|
+让不同类型通过相同的方法约定被统一使用。掌握接口声明、隐式实现、接口参数与切片、值和指针的方法集,以及最基本的 nil 接口区别。
|
|
|
+
|
|
|
+先手敲 [main.go](./main.go),运行成功后再看 [main_test.go](./main_test.go)。本课共享根目录的 `go.mod`,只用标准库,无需安装依赖。不引入泛型约束、类型断言、类型 switch 或反射。
|
|
|
+
|
|
|
+## 1. 接口描述行为
|
|
|
+
|
|
|
+```go
|
|
|
+type Introducer interface {
|
|
|
+ Introduce() string
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+这是一个接口类型,要求实现者提供“没有参数、返回 string”的 Introduce 方法。接口中写方法签名,不写方法体,也不写学生姓名等数据字段。
|
|
|
+
|
|
|
+```go
|
|
|
+func (student Student) Introduce() string {
|
|
|
+ return "我是学生:" + student.Name
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+Student 具备这个方法,就自动实现 Introducer,不需要写 implements,也不是通过继承某个基类实现。
|
|
|
+
|
|
|
+方法名、参数和返回值类型必须匹配;只有一个同名结构体字段或包级函数,不算实现接口。大小写也有区别,introduce 不等于 Introduce。
|
|
|
+
|
|
|
+## 2. 函数接收接口
|
|
|
+
|
|
|
+```go
|
|
|
+func introduction(person Introducer) string {
|
|
|
+ return person.Introduce()
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+传入学生时调用学生的方法,传入老师时调用老师的方法。函数只依赖“能自我介绍”这个能力,不依赖具体字段。
|
|
|
+
|
|
|
+这就是一种多态:相同调用,具体行为由实际类型决定。函数仍然按值传参,不会因为参数是接口就变成引用传递。
|
|
|
+
|
|
|
+接口不一定越多越好。简单、固定的计算函数不必为了形式而抽象;当确实存在多个实现,或调用方只需要很少的方法时,小接口才有价值。
|
|
|
+
|
|
|
+## 3. 静态接口类型与动态具体类型
|
|
|
+
|
|
|
+```go
|
|
|
+var person Introducer = Student{Name: "小朱"}
|
|
|
+person = Teacher{Name: "王老师", Subject: "Go"}
|
|
|
+```
|
|
|
+
|
|
|
+person 变量的静态类型始终是 Introducer,内部保存的具体值和动态类型可以变化。`fmt.Printf("%T", person)` 显示动态具体类型。
|
|
|
+
|
|
|
+可以先按“接口值包含具体类型和对应值”理解,不要把这当成必须依赖的内存布局。
|
|
|
+
|
|
|
+通过接口变量只能直接调用该接口声明的方法;即使具体学生有 Name 字段,也不能写 `person.Name`。需要具体类型特有能力时,后续再学习类型断言。
|
|
|
+
|
|
|
+## 4. 接口切片
|
|
|
+
|
|
|
+```go
|
|
|
+people := []Introducer{student, teacher, &Assistant{Name: "小陈"}}
|
|
|
+```
|
|
|
+
|
|
|
+每个元素都必须能作为 Introducer 使用,但具体类型可以不同。循环通过相同接口调用即可,不需要对每个类型写 if 分支。
|
|
|
+
|
|
|
+注意 `[]Student` 不会自动转换为 `[]Introducer`;需要逐个元素赋值或 append,因为两个切片的元素类型不同。
|
|
|
+
|
|
|
+## 5. 值接收者与指针接收者
|
|
|
+
|
|
|
+本课三种实现:
|
|
|
+
|
|
|
+| 类型 | 方法接收者 | 能赋给 Introducer 的值 |
|
|
|
+| --- | --- | --- |
|
|
|
+| Student | Student | Student 和 *Student |
|
|
|
+| Teacher | Teacher | Teacher 和 *Teacher |
|
|
|
+| Assistant | *Assistant | 只有 *Assistant |
|
|
|
+
|
|
|
+对于本课这种普通命名类型 T,T 的方法集包含接收者为 T 的方法;*T 的方法集同时包含接收者为 T 和 *T 的方法。
|
|
|
+
|
|
|
+因此:
|
|
|
+
|
|
|
+```go
|
|
|
+var person Introducer = &Assistant{Name: "小陈"} // 正确。
|
|
|
+// var person Introducer = Assistant{Name: "小陈"} // 错误。
|
|
|
+```
|
|
|
+
|
|
|
+上一课说可取地址变量可以自动调用指针方法,但那是**直接方法调用的便利规则**,不会让 Assistant 值在接口赋值时自动变成 *Assistant。
|
|
|
+
|
|
|
+不必死背所有方法集边界,先能解释本课这三种实现即可。
|
|
|
+
|
|
|
+## 6. nil 接口与包含 nil 指针的接口
|
|
|
+
|
|
|
+```go
|
|
|
+var empty Introducer
|
|
|
+```
|
|
|
+
|
|
|
+没有具体类型、也没有具体值,因此 empty 等于 nil,不能调用其方法。
|
|
|
+
|
|
|
+```go
|
|
|
+var assistant *Assistant
|
|
|
+var person Introducer = assistant
|
|
|
+```
|
|
|
+
|
|
|
+assistant 是 nil 指针,但 person 中仍保存了动态类型 *Assistant,所以 person 不等于 nil。
|
|
|
+
|
|
|
+本课 Assistant.Introduce 主动检查了 nil 接收者,因此这个调用能安全返回提示。不代表所有实现都支持 nil;有些方法会访问字段并 panic。仅检查 `person != nil` 不能保证其中没有 nil 指针。
|
|
|
+
|
|
|
+这是 Go 常见易错点,本课先观察现象即可。特别是返回 error 时,通常应直接 `return nil` 表示无错误,不要把某个 nil 具体指针装进 error 接口后误以为它仍等于 nil。
|
|
|
+
|
|
|
+## 7. error 也是接口
|
|
|
+
|
|
|
+前面的 error 可以先理解成:
|
|
|
+
|
|
|
+```go
|
|
|
+type error interface {
|
|
|
+ Error() string
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+这是预定义接口的定义形式示意,**不要在本课自行重新声明它**。
|
|
|
+
|
|
|
+具备 Error() string 方法的类型就能作为 error 使用。这解释了为什么 strconv 的转换错误和 errors.New 的错误,都能通过 error 返回。自定义错误类型留到进阶练习。
|
|
|
+
|
|
|
+## 8. 接口为什么有利于测试
|
|
|
+
|
|
|
+测试里定义了 fakeIntroducer:它只返回预先设置的固定文本,也满足接口。
|
|
|
+
|
|
|
+```go
|
|
|
+func (fake fakeIntroducer) Introduce() string {
|
|
|
+ return fake.text
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+把它传给 introduction,就能验证函数是否调用并返回接口方法的结果,不需要真实学生或老师。这个小例子只是演示机制,业务逻辑很少,测试收益有限。
|
|
|
+
|
|
|
+真实项目中,同样的思路可以让业务依赖小型存储接口,用内存实现替代外部数据库,或避免测试时发送真实通知。并不是所有测试都需要替身,也不必为每个类型都创建接口。
|
|
|
+
|
|
|
+测试中的 `t.Fatal` 与 `t.Fatalf` 一样会标记失败并停止当前测试,只是 Fatal 不使用格式占位符。
|
|
|
+
|
|
|
+## 编译并运行
|
|
|
+
|
|
|
+在**仓库根目录**执行:
|
|
|
+
|
|
|
+```powershell
|
|
|
+New-Item -ItemType Directory -Force bin/16_interfaces | Out-Null
|
|
|
+go build -o ./bin/16_interfaces/interfaces.exe ./lessons/16_interfaces
|
|
|
+.\bin\16_interfaces\interfaces.exe
|
|
|
+```
|
|
|
+
|
|
|
+修改源码后需要重新编译。快速编译并运行:
|
|
|
+
|
|
|
+```powershell
|
|
|
+go run ./lessons/16_interfaces
|
|
|
+```
|
|
|
+
|
|
|
+或从根目录进入本课:
|
|
|
+
|
|
|
+```powershell
|
|
|
+Set-Location lessons/16_interfaces
|
|
|
+go run .
|
|
|
+```
|
|
|
+
|
|
|
+## 预期输出
|
|
|
+
|
|
|
+```text
|
|
|
+=== 不同类型实现同一个接口 ===
|
|
|
+我是学生:小朱
|
|
|
+我是老师:王老师,教授Go
|
|
|
+
|
|
|
+=== 接口变量与动态类型 ===
|
|
|
+实际类型:main.Student,介绍:我是学生:小朱
|
|
|
+实际类型:main.Teacher,介绍:我是老师:王老师,教授Go
|
|
|
+
|
|
|
+=== 接口切片 ===
|
|
|
+我是学生:小朱
|
|
|
+我是老师:王老师,教授Go
|
|
|
+我是助教:小陈
|
|
|
+
|
|
|
+=== 接口与 nil ===
|
|
|
+未赋值接口为 nil:true
|
|
|
+保存 nil 指针后,接口为 nil:false
|
|
|
+助教信息为空
|
|
|
+```
|
|
|
+
|
|
|
+## 测试与练习
|
|
|
+
|
|
|
+在仓库根目录执行测试:
|
|
|
+
|
|
|
+```powershell
|
|
|
+go test -count=1 -v ./lessons/16_interfaces
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] 新增 Robot 类型,实现 Introduce() string,再加入 people。
|
|
|
+- [ ] 给一个实现增加额外字段,观察 introduction 是否需要修改。
|
|
|
+- [ ] 临时将 Assistant 值而不是指针赋给接口,观察编译错误,再恢复。
|
|
|
+- [ ] 给 fakeIntroducer 换一段文本,观察测试结果。
|
|
|
+- [ ] 对比未赋值接口和包含 nil 指针的接口,解释为什么 nil 判断不同。
|
|
|
+
|
|
|
+若另行手敲,放到本课的 `practice/` 子目录,用 `go run ./lessons/16_interfaces/practice` 运行,不要在同一个包中重复声明 main 或同名类型。
|
|
|
+
|
|
|
+下一课学习文件读写与路径操作,将数据从内存保存到文件。
|
|
|
+
|
|
|
+[上一课:包与模块](../15_packages/README.md) · [学习总览](../../README.md)
|