Go 中如何实现多态

众所周知,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 运行的逻辑是:

  1. d.Say()
  2. 子类没有 Say() 方法,去找父类 Animal, 并调用 Animal.Say()
  3. 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

即使实际对象是 *DogGo 也不会做虚函数分派

嵌入(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()
}

解析:

  1. 定义接口
type Eater interface {
    Eate()
}
  1. Animal 持有接口,而不是“调用自己”
type Animal struct {
    name  string
    eater Eater
}

func (a *Animal) Say() {
    fmt.Printf("Animal Say():%s\n", a.name)
    a.eater.Eate()
}
  1. Dog 实现
type Dog struct {
    Animal
}

func (d *Dog) Eate() {
    fmt.Printf("Dog::Eate() %s eat\n", d.name)
}
  1. 初始化时“注入自己”(关键)
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,转载请注明出处。

发表评论