9.1 接口定义与实现
本篇学习 Go 接口的定义、隐式实现与多态调用,并能用接口约束对象行为。
接口定义与实现
概念说明
接口(interface)用于描述“行为”。
只要某个类型拥有接口要求的全部方法,它就自动满足这个接口。
Go 不需要显式写 implements。
implements 可以理解为“声明某个类型实现了某个接口”,一些语言需要这样写,但 Go 只看方法是否匹配。
这种方式叫隐式实现,也就是不写额外声明,只要方法集匹配就自动满足接口。
关键规则
- 接口里只声明方法签名,不写方法体。
- 类型只要方法匹配,就自动实现接口。
- 接口变量可以保存任意满足该接口的值。
- 接口适合让同一段代码处理多种类型。
语法模板
1
2
3
| type 接口名 interface {
方法名(参数列表) 返回值列表
}
|
示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
| package main
import "fmt"
type Speaker interface { // 只要有 Speak() string 方法,就满足 Speaker 接口
Speak() string
}
type Dog struct {
Name string
}
func (dog Dog) Speak() string { // Dog 实现 Speaker 接口
return dog.Name + ": wang"
}
type Cat struct {
Name string
}
func (cat Cat) Speak() string { // Cat 也实现 Speaker 接口
return cat.Name + ": miao"
}
func say(speaker Speaker) { // 接收任何满足 Speaker 接口的值
fmt.Println(speaker.Speak())
}
func main() {
say(Dog{Name: "dog"}) // Dog{Name: "dog"} 是创建结构体值,不是函数调用
say(Cat{Name: "cat"})
}
|
输出结果:
怎么理解这个示例
say(speaker Speaker) 接收的不是固定结构体,而是任何满足 Speaker 接口的值。Dog{Name: "dog"} 不是 Dog() 函数调用,而是创建一个 Dog 结构体值。- 下面这段展开写法和
say(Dog{Name: "dog"}) 的理解方式是等价的:
1
2
3
4
| dog := Dog{Name: "dog"} // 创建一个 Dog 类型的值
var speaker Speaker = dog // 放进接口变量,因为 Dog 实现了 Speaker
fmt.Println(speaker.Speak()) // 实际调用 Dog 的 Speak 方法
|
speaker.Speak() 调用的是接口里实际保存值对应的方法。保存 Dog 就调用 Dog.Speak(),保存 Cat 就调用 Cat.Speak()。
什么时候使用接口
多个类型要完成同一行为时,就可以考虑接口。
- 有多个不同类型,要做同一件事。
- 你希望调用方只关心“能不能做”,不关心“具体是什么类型”。
- 以后可能还会新增更多同类实现。
场景:发送通知
假设程序现在支持邮件通知和短信通知。它们的共同点都是“发送通知”,只是实现方式不同。
不使用接口时
不用接口时,通常会为每种类型单独写函数:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
| type Email struct{}
func (email Email) Send(message string) {
fmt.Println("邮件通知:", message)
}
type SMS struct{}
func (sms SMS) Send(message string) {
fmt.Println("短信通知:", message)
}
func sendEmail(email Email, message string) { // 只能处理 Email
email.Send(message)
}
func sendSMS(sms SMS, message string) { // 只能处理 SMS
sms.Send(message)
}
|
调用时要分别写:
1
2
| sendEmail(Email{}, "订单已支付")
sendSMS(SMS{}, "验证码是 1234")
|
问题在于:
- 每增加一种通知方式,往往就要多写一个函数。
- 调用代码不统一,函数名会越来越多。
- 以后新增微信通知,还得继续补
sendWeChat。
使用接口时
把“发送通知”抽成接口后,可以统一处理:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
| type Notifier interface {
Send(message string)
}
type Email struct{}
func (email Email) Send(message string) {
fmt.Println("邮件通知:", message)
}
type SMS struct{}
func (sms SMS) Send(message string) {
fmt.Println("短信通知:", message)
}
func notify(notifier Notifier, message string) { // 统一接收所有满足 Notifier 的类型
notifier.Send(message)
}
|
调用时统一写成:
1
2
| notify(Email{}, "订单已支付")
notify(SMS{}, "验证码是 1234")
|
为什么更方便
- 调用方式统一,只需要
notify(...)。 - 新增实现时,通常不用改原来的
notify 函数。 - 调用方只关心“能不能发送通知”,不关心具体类型。
如果以后新增微信通知,只要实现同样的方法即可:
1
2
3
4
5
6
7
| type WeChat struct{}
func (wechat WeChat) Send(message string) {
fmt.Println("微信通知:", message)
}
notify(WeChat{}, "你有一条新消息") // 新类型只要实现同样方法,也能直接复用 notify
|
什么时候适合用接口
适合:
- 多个类型有相同行为。
- 希望同一个函数处理多种类型。
- 代码以后可能扩展新的实现。
暂时不一定要用:
- 只有一个具体类型。
- 业务非常简单,不存在“多种实现”。
- 为了用接口而用接口,反而会让代码更绕。
常见错误
- 以为 Go 需要显式声明实现接口,实际只看方法集合是否匹配。
- 把
Dog{Name: "dog"} 误认为函数调用,实际它是创建 Dog 结构体值。 - 方法名、参数或返回值有一点不同,就不能满足接口。
- 把接口当成“任意类型容器”使用,忽略接口本质是行为约束。