主题
第07章 接口与多态 —— Go 的鸭子类型
本章目标:理解 Go 接口"隐式实现"的设计哲学,掌握类型断言、空接口、类型 switch,知道如何用接口写出可扩展的代码。
1. 接口是什么?
🌰 生活化比喻:接口是契约——只描述"能做什么",不关心"是谁做的"。
举个例子:你需要找一个"能开车"的人。
- 你不在乎他叫小明还是小红
- 你只在乎他会开车
go
type Driver interface {
Drive()
}任何只要有 Drive() 方法的类型,都自动实现了 Driver 接口。
2. 隐式实现(Duck Typing)
go
type Animal interface {
Speak() string
}
type Dog struct{}
func (d Dog) Speak() string { return "汪汪" }
type Cat struct{}
func (c Cat) Speak() string { return "喵喵" }
// Dog 和 Cat 都"自动"实现了 Animal,无需声明
var a Animal = Dog{}
fmt.Println(a.Speak()) // 汪汪
a = Cat{}
fmt.Println(a.Speak()) // 喵喵🌰 生活化比喻:"如果它走起来像鸭子、叫起来像鸭子,那它就是鸭子"——这就是 Duck Typing。
与 Java/C# 对比
| 语言 | 实现方式 |
|---|---|
| Java | class Dog implements Animal 必须显式声明 |
| Go | 只要有方法就隐式实现 |
好处:
- 解耦:定义接口的包不需要知道实现者
- 灵活:可以为别人写的类型补充接口实现
📊 配套图解:diagrams/duck-typing.svg
3. 接口的内部结构
接口变量在内存中是个两元组:
┌─────────────────────┐
│ Type → 实际类型描述符
│ Value → 实际值的指针
└─────────────────────┘go
var a Animal = Dog{Name: "旺财"}
// a 内部:{ Type: Dog, Value: 指向 Dog{Name:"旺财"} }4. 空接口 interface{} 与 any
interface{}(Go 1.18+ 推荐用 any)是没有任何方法的接口——所有类型都满足它,等于"任意类型"。
go
func print(v any) {
fmt.Println(v)
}
print(42)
print("hello")
print([]int{1, 2, 3})🌰 生活化比喻:any 像万能口袋,啥都能装。
⚠️ 不要滥用 any:失去类型安全,编译器帮不了你。除非确实需要"任意类型"(如标准库的 fmt.Println),否则用具体类型。
5. 类型断言(Type Assertion)
把接口"还原"成具体类型:
go
var a any = "hello"
s := a.(string) // 断言为 string,失败会 panic
s, ok := a.(string) // 安全断言:失败时 ok = false完整示例:
go
func describe(v any) {
if s, ok := v.(string); ok {
fmt.Println("是字符串:", s)
} else if n, ok := v.(int); ok {
fmt.Println("是整数:", n)
} else {
fmt.Println("未知类型")
}
}6. 类型 switch
类型断言的 if-else 链可以用 switch x.(type) 优雅化:
go
func describe(v any) {
switch x := v.(type) {
case nil:
fmt.Println("nil")
case int:
fmt.Println("int", x)
case string:
fmt.Println("string", x)
case []int:
fmt.Println("int slice", x)
case error:
fmt.Println("error", x.Error())
default:
fmt.Printf("未知类型 %T\n", x)
}
}📊 配套图解:diagrams/type-assertion.svg
7. 接口组合
go
type Reader interface {
Read(p []byte) (n int, err error)
}
type Writer interface {
Write(p []byte) (n int, err error)
}
// 接口可以嵌入其他接口
type ReadWriter interface {
Reader
Writer
}这就是标准库 io.ReadWriter 的定义。
8. 著名例子:io.Reader 和 io.Writer
Go 标准库最经典的接口设计:
go
type Reader interface {
Read(p []byte) (n int, err error)
}
type Writer interface {
Write(p []byte) (n int, err error)
}只有一个方法,但天底下所有读/写操作都能套进去:
- 文件:
*os.File - 网络连接:
net.Conn - 字符串读取:
strings.Reader - 缓冲:
bytes.Buffer - HTTP 请求体:
http.Request.Body
go
io.Copy(dst Writer, src Reader) // 从任意 Reader 拷贝到任意 Writer🌰 生活化比喻:io.Reader/Writer 就像通用插头——所有电器都用同一种插头标准。
9. nil 接口的"诡异"行为
go
type MyError struct{ msg string }
func (e *MyError) Error() string { return e.msg }
func mightFail() error {
var p *MyError = nil
return p // 返回 (类型=*MyError, 值=nil) 的接口,而非 nil 接口!
}
func main() {
err := mightFail()
fmt.Println(err == nil) // false !踩坑了
}原因:接口的 nil 判断同时比较类型和值。(*MyError)(nil) 不是 nil,因为类型部分不为空。
正确写法:
go
func mightFail() error {
var p *MyError = nil
if p != nil {
return p
}
return nil
}⚠️ 这是 Go 面试的高频陷阱题!
10. 案例:可扩展的支付系统
go
type Payment interface {
Pay(amount float64) error
}
type Alipay struct{}
func (a Alipay) Pay(amt float64) error {
fmt.Printf("支付宝支付 ¥%.2f\n", amt)
return nil
}
type WeChatPay struct{}
func (w WeChatPay) Pay(amt float64) error {
fmt.Printf("微信支付 ¥%.2f\n", amt)
return nil
}
func Checkout(p Payment, amt float64) error {
return p.Pay(amt)
}
// 使用
Checkout(Alipay{}, 99.9)
Checkout(WeChatPay{}, 99.9)新增"信用卡支付"?只要写一个有 Pay 方法的类型即可,不用修改 Checkout。这就是接口的威力。
11. 本章小结
✅ 接口只描述"能做什么",不关心"是谁做的"
✅ Go 是隐式实现——只要方法对得上就算实现
✅ 接口变量底层是 (Type, Value) 二元组
✅ any = interface{} = 任意类型
✅ 类型断言 v.(T),安全版 v, ok := i.(T)
✅ 类型 switch 处理多种可能类型
✅ nil 接口陷阱:(*T)(nil) != nil interface
✅ 接口组合通过嵌入实现