Go 面向对象(中):接口与鸭子类型
Go 的接口是隐式的——你不需要声明”我实现了某个接口”。这叫鸭子类型:如果它走起路来像鸭子,叫起来像鸭子,那它就是鸭子。但这个设计也埋了一些坑。
Q1:Go 的接口和 Java 的 interface 有什么本质区别?
A: 一张表说清楚:
| 特性 | Java Interface | Go Interface |
|---|---|---|
| 实现方式 | class Dog implements Animal 显式声明 |
隐式满足,无需声明 |
| 方法集 | 必须实现所有方法 | 同左,但编译器自动检测 |
| 值/指针语义 | 只有引用类型 | 值接收者 vs 指针接收者有区别(大坑!) |
| 空接口 | 无等价物 | interface{} 可存任意值 |
| 接口组合 | extends 多继承 |
嵌入接口 |
1 | // 定义接口 |
这就是隐式接口的核心:你只管写你的结构体和方法,接口在需要的地方自然被满足。
Q2:那个传说中的”值接收者 vs 指针接收者”坑到底怎么回事?
A: 这是 Go 接口最危险的陷阱,没有之一:
1 | type Mover interface { |
看起来都正常?看这里:
1 | c := Car{} |
规则:
1 | *T 实现 I → T 和 *T 都满足 I |
实战建议:如果结构体可能通过接口使用,统一用指针接收者,避免这种不一致。
Q3:interface{} 空接口怎么用?不是什么都能塞进去吗?
A: 对,interface{} 可以存任何值,但取出来的时候要小心:
1 | var anything interface{} |
取出来需要类型断言(Type Assertion):
1 | anything := 42 |
type switch —— 更优雅的多类型处理
1 | func describe(v interface{}) { |
泛型出现后的变化
Go 1.18 之后,能用泛型(any)的场景尽量别用空接口了:
1 | // 旧写法(Go 1.17 及之前) |
Q4:error 本质就是一个接口?
A: 对,这是理解 Go 错误处理的钥匙:
1 | // error 就是这个内置接口: |
就这么简单。所以任何实现了 Error() string 的类型都可以当 error 用:
1 | // 自定义错误类型 |
错误包装(Error Wrapping)是 Go 1.13+ 的标准做法:
1 | // %w 格式化,保留错误链 |
Q5:接口可以组合吗?
A: 可以,而且非常优雅——嵌入接口:
1 | type Reader interface { |
只要同时实现了 Read() 和 Write() 的类型就自动满足 ReadWriter。这就是标准库 io.ReadWriter 的定义方式。
对比 Java 的多接口实现:
1 | // Java:显式 implements |
Q6:nil 接口是什么鬼?我见过 interface{}(nil) != nil 这种 bug!
A: 这是个经典面试题:
1 | var p *Dog // p == nil (指针为 nil) |
为什么?
接口内部由两部分组成 (type, value):
p == nil→(type=nil, value=nil)i = p后 →(type=*Dog, value=nil)- 接口判 nil 要求 type 和 value 都为 nil
- 现在 type 不是 nil 了,所以 i 不是 nil!
1 | func process(data interface{}) { |
防御写法:
1 | func process(data interface{}) { |
或者更简单——不要传 nil 指针进 interface{},设计上避免这种情况。
Q7:什么时候该定义接口?什么时候不该?
A: Go 社区有个原则叫 Accept interfaces, return structs(接受接口,返回结构体):
| 场景 | 建议 |
|---|---|
| 函数参数 | 用接口(解耦) |
| 返回值 | 用具体结构体(灵活性留给调用方) |
| 包的边界(导出 API) | 定义接口 |
| 包的内部 | 不需要接口,直接用具体类型 |
1 | // ✅ 好:参数用接口 |
经验法则:如果你发现自己在问”要不要加个接口”,答案通常是先不加。等到真的有第二个实现时再提取接口也不迟——Go 的隐式接口让你重构零成本。
下期预告
下一篇聊 组合优于继承——Go 没有 extends 怎么做代码复用?嵌入的多种模式、以及什么时候该用接口替代组合。然后我们转入工程实践篇:error 处理最佳实践。
TODO: 回顾一下你 slg-go 项目里的接口定义——有没有哪个地方用了不必要的接口?或者反过来,某个函数参数应该用接口却写了具体类型?