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