接口与组合
Go 用隐式接口与组合表达多态,而不是类继承树。
接口与隐式实现
接口只声明方法集;类型只要实现了全部方法,就自动满足该接口,无需 implements:
type Speaker interface {
Speak() string
}
type Dog struct{ Name string }
func (d Dog) Speak() string {
return d.Name + ": woof"
}
func say(s Speaker) {
fmt.Println(s.Speak())
}
say(Dog{Name: "Buddy"})
接口值内部是「动态类型 + 动态值」。两边都为 nil 时接口才是 nil——常见坑:
var d *Dog = nil
var s Speaker = d
fmt.Println(s == nil) // false:接口非 nil,动态类型是 *Dog
any / interface
空接口可承接任意值(Go 1.18+ 别名 any):
var v any = 42
v = "hi"
switch x := v.(type) {
case int:
fmt.Println("int", x)
case string:
fmt.Println("string", x)
default:
fmt.Println("other")
}
类型断言:x, ok := v.(string)。能用具体类型或小接口就不要滥用 any。
组合(embedding)优于继承
结构体可嵌入另一类型,提升其字段/方法:
type Engine struct{}
func (Engine) Start() { fmt.Println("start") }
type Car struct {
Engine
Brand string
}
c := Car{Brand: "X"}
c.Start() // 提升自 Engine
这是组合:Car 有一个 Engine,不是「Car is-a Engine」的继承树。也可嵌入接口。
小接口习惯
标准库偏爱小接口,例如:
// 简化示意
type Reader interface {
Read(p []byte) (n int, err error)
}
实现一个方法就能接入大量 I/O 工具。设计自己的接口时:
- 站在调用方需要什么行为来定义
- 接口尽量小;大接口难实现、难 mock
- 接受接口、返回具体类型(常见经验法则)
方法集提醒:指针接收者 实现的方法,只有 *T 满足接口;值接收者则 T 与 *T 都可能满足。
小结
- 实现是隐式的,看方法集是否匹配
any要配合类型断言 / 类型 switch- 用 embedding 做组合,不建深继承
- 优先小接口,向标准库对齐