众所周知,Go 不支持面向对象,更不支持多态了。但在实际的编码过程中,是需要用到“多态”这个技巧的。
一、问题
比如如下代码:
// animal.go
package up
import (
"fmt"
)
type Animal struct {
name string
}
func (a *Animal) Say() {
fmt.Printf("Animal Say():%s\n", a.name)
a.Eate()
}
func (a *Animal) Eate() {
fmt.Printf("Animal::Eate() %s eate\n", a.name)
}
// dog.go
package up
import (
"fmt"
)
type Dog struct {
Animal
}
func NewDog(name string) *Dog {
d := &Dog{}
d.name = name
return d
}
func (d *Dog) Eate() {
fmt.Printf("Dog::Eate() %s eate\n", d.name)
}
// main.go
package main
import "dao/cmd/test/up"
func main() {
d := up.NewDog("wangcai")
d.Say()
}
我的本意是通过 Dog 子类 Eate 重写父类的 Eate,在 main 中调用 d.Say() 时,d.Say() 会调用父类的 Say (子类没重写),父类里又调用了 a.Eate(),按通常的面向对象语言,此时如果子类有重写 Eate 方法的话,这时会调用子类的 Eate (多态)。但在 Go 中运行的逻辑并不是这样的,Go 运行的逻辑是:
- d.Say()
- 子类没有 Say() 方法,去找父类 Animal, 并调用 Animal.Say()
- Animal.Say() 中调用 a.Eate()。在 Go 中会使用声明类型来调用,即此时调用的是 (*Animal).Eate() ,并不会根据实际的运行时类型调用(即子类Dog)
所以最后的结果是:
Animal Say():wangcai
Animal::Eate() wangcai eate
问题的核心点是:
方法并不是“虚函数”,嵌入 ≠ 多态。
Go 的方法调用是“静态绑定”,不是运行时多态。
func (a *Animal) Say() {
a.Eate()
}
这里的 a的 静态类型 是 *Animal,所以早在编译期就已经确定:
a.Eate() ==> (*Animal).Eate
即使实际对象是 *Dog,Go 也不会做虚函数分派。
嵌入(Embedding) ≠ 继承(Inheritance)
type Dog struct {
Animal
}
这只是 字段提升(promotion),并不是 OOP 里的继承:
Dog拥有Animal的字段和方法Animal不知道Dog的存在Animal的方法不会被子结构“覆盖”
所以 Animal.Say()内部调用的,永远是 Animal.Eate()
二、解决方案
那如何实现“多态”的效果呢?有两种方案:
方案一:使用接口 interface(常用方式)
Go 的多态只发生在接口调用上
代码:
// animal.go
package up
import (
"fmt"
)
type Eater interface {
Eate()
}
type Animal struct {
name string
eater Eater
}
func (a *Animal) Say() {
fmt.Printf("Animal Say():%s\n", a.name)
a.eater.Eate()
}
func (a *Animal) Eate() {
fmt.Printf("Animal::Eate() %s eate\n", a.name)
}
// dog.go
package up
import (
"fmt"
)
type Dog struct {
Animal
}
func NewDog(name string) *Dog {
d := &Dog{}
d.name = name
d.eater = d
return d
}
func (d *Dog) Eate() {
fmt.Printf("Dog::Eate() %s eate\n", d.name)
}
// main.go
package main
import "dao/cmd/test/up"
func main() {
d := up.NewDog("wangcai")
d.Say()
}
解析:
- 定义接口
type Eater interface {
Eate()
}
- Animal 持有接口,而不是“调用自己”
type Animal struct {
name string
eater Eater
}
func (a *Animal) Say() {
fmt.Printf("Animal Say():%s\n", a.name)
a.eater.Eate()
}
- Dog 实现
type Dog struct {
Animal
}
func (d *Dog) Eate() {
fmt.Printf("Dog::Eate() %s eat\n", d.name)
}
- 初始化时“注入自己”(关键)
func NewDog(name string) *Dog {
d := &Dog{}
d.name = name
d.eater = d
return d
}
如此,输出:
Animal Say():wangcai
Dog::Eate() wangcai eate
方案二:函数指针 / 函数字段注入
代码如下:
// animal.go
package up
import (
"fmt"
)
type Animal struct {
name string
Eate func()
}
func (a *Animal) Say() {
fmt.Printf("Animal Say():%s\n", a.name)
a.Eate()
}
func (a *Animal) eate() {
fmt.Printf("Animal::Eate() %s eate\n", a.name)
}
// dog.go
package up
import (
"fmt"
)
type Dog struct {
Animal
}
func NewDog(name string) *Dog {
d := &Dog{}
d.name = name
d.Eate = d.eate
return d
}
func (d *Dog) eate() {
fmt.Printf("Dog::Eate() %s eate\n", d.name)
}
// main.go (没改变,略)
详解:
这种写法是:
把“可变行为”作为函数值,注入到结构体中
也就是典型的:
- Strategy Pattern (策略模式)
- 函数式依赖注入(Function Injection)
调用链:
d := up.NewDog("wangcai")
d.Say()
展开后等价于:
d.Animal.Say()
-> a.Eate()
-> d.eate()
- 这是真正的运行期动态分派
- 不依赖 interface
- 不依赖方法集
两种方案的对比
| 维度 | interface 方案 | 函数字段方案 |
|---|---|---|
| 多态方式 | 接口方法表 | 函数指针 |
| 绑定时机 | 运行期(接口赋值) | 初始化时 |
| 行为完整性 | 强约束 | 弱约束 |
| 可维护性 | ⭐⭐⭐⭐⭐ | ⭐⭐⭐ |
| 可读性 | ⭐⭐⭐⭐⭐ | ⭐⭐⭐ |
| 误用风险 | 低 | 高 |
| nil 安全 | 高 | 低(容易 panic) |
| 工程常见度 | 非常高(主流) | 较少 |
| 单元测试 | 极佳 | 尚可 |
| 热插拔 | 好 | 极好 |
三、最后
所以要用 Go实现多态的效果,不能用以前的面向对象的思维来操作。同时对以上的分析,要深刻理解多态在 Go中的具体实现方法,以及优缺点。
ps:
以上分析,是基于我最近使用 Go 来重写 车联网IoT 这部分,有遇到了这种问题,从而进行分析和学习
本文为原创内容,作者:闲鹤,原文链接:https://blog.uwenya.cc/1623.html,转载请注明出处。