跳到主要内容

Go 中的继承与多态

Go 没有传统面向对象语言里的继承机制,而是通过 结构体组合(嵌入)接口 来实现代码复用与多态。理解这一点是学习 Go 面向对象编程的关键。


1. 基本概念

  • 组合 (Composition) Go 推荐使用 结构体嵌入 来实现代码复用,而不是继承。
  • 接口 (Interface) Go 的多态是基于接口的。只要一个类型实现了接口定义的方法,就可以作为该接口的实例。
  • 方法分派 (Method Dispatch)
    • 接口调用方法 → 运行时根据动态类型进行分派(真正的多态)。
    • 结构体方法内部调用 → 只根据接收者的静态类型,不会自动转发到“子类覆盖”的方法。

2. 常见模式

在 Go 中,想要实现类似“继承 + 虚函数”的效果,通常有三种模式:

  1. 子类完全重写父类方法
  2. 父类持有函数指针(回调方式)
  3. 父类持有接口(Self Reference 技巧)

下面分别介绍。


2.1 子类完全重写父类方法(最直观方式)

子类在实现接口时,直接提供完整逻辑,不依赖父类方法。

示例代码

package main

import "fmt"

// DataCollector 定义数据收集器接口
type DataCollector interface {
CollectData()
}

// BaseCollector 基础收集器
type BaseCollector struct{}

func (b *BaseCollector) CollectData() {
fmt.Println("BaseCollector: 收集数据")
}

// NetworkCollector 网络收集器
type NetworkCollector struct{}

// 子类完全实现接口,不依赖 BaseCollector 的实现
func (n *NetworkCollector) CollectData() {
fmt.Println("NetworkCollector: 收集网络数据")
}

func main() {
var c DataCollector

c = &BaseCollector{}
c.CollectData()

c = &NetworkCollector{}
c.CollectData()
}

// Output:
// BaseCollector: 收集数据
// NetworkCollector: 收集网络数据

特点

  • 简单直接。
  • 父类的实现完全不会被使用。
  • 适合子类逻辑与父类差异很大的场景。

2.2 父类持有函数指针(回调方式)

父类提供框架逻辑,并在关键步骤调用一个 回调函数指针。子类只需将自己的方法注入父类,即可在父类逻辑中“回调”子类方法。

示例代码

package main

import "fmt"

// DataCollector 接口
type DataCollector interface {
CollectData()
}

// BaseCollector 基础收集器
type BaseCollector struct {
doCollect func()
}

func (b *BaseCollector) CollectData() {
fmt.Println("BaseCollector: 开始收集数据")
b.ProcessData()
}

func (b *BaseCollector) ProcessData() {
fmt.Println("BaseCollector: 处理数据")
if b.doCollect != nil {
b.doCollect() // 回调子类逻辑
}
}

// NetworkCollector 网络收集器
type NetworkCollector struct {
*BaseCollector
}

func (n *NetworkCollector) ProcessData() {
fmt.Println("NetworkCollector: 处理网络数据")
}

func main() {
networkCollector := &NetworkCollector{
BaseCollector: &BaseCollector{},
}

// 注入子类方法
networkCollector.doCollect = networkCollector.ProcessData

networkCollector.CollectData()
}

// Output:
// BaseCollector: 开始收集数据
// BaseCollector: 处理数据
// NetworkCollector: 处理网络数据

特点

  • 父类提供框架,子类只需注入自定义逻辑。
  • 不依赖接口,更加灵活。
  • 缺点是需要手动注入回调,易出错。

2.3 父类持有接口(Self Reference 技巧)

父类中持有一个接口类型的指针(通常是自己),在方法中通过接口调用,从而触发动态分派。

示例代码

package main

import "fmt"

// DataCollector 接口
type DataCollector interface {
CollectData()
ProcessData()
}

// BaseCollector 基础收集器
type BaseCollector struct {
owner DataCollector // 持有接口,指向自己或子类
}

func (b *BaseCollector) CollectData() {
fmt.Println("BaseCollector: 开始收集数据")
b.owner.ProcessData() // 通过接口调用,触发动态分派
}

func (b *BaseCollector) ProcessData() {
fmt.Println("BaseCollector: 处理数据")
}

// NetworkCollector 网络收集器
type NetworkCollector struct {
*BaseCollector
}

func (n *NetworkCollector) ProcessData() {
fmt.Println("NetworkCollector: 处理网络数据")
}

// 构造函数:注入 self reference
func NewNetworkCollector() *NetworkCollector {
n := &NetworkCollector{}
base := &BaseCollector{owner: n}
n.BaseCollector = base
return n
}

func main() {
collector := NewNetworkCollector()
var c DataCollector = collector
c.CollectData()
}

// Output:
// BaseCollector: 开始收集数据
// NetworkCollector: 处理网络数据

特点

  • 父类逻辑可复用,子类方法能动态分派。
  • 模拟了传统 OOP 的“虚函数”效果。
  • 缺点是需要写构造函数,并小心循环引用问题。

3. 三种模式对比

模式特点适用场景
子类完全重写方法简单直接,父类逻辑完全不用子类逻辑与父类完全不同
函数指针回调方式父类框架+子类注入逻辑,灵活父类框架为主,子类只定制部分逻辑
Self Reference 技巧父类逻辑可复用,支持动态分派类似模板方法模式,子类覆盖部分逻辑

4. 总结

  • Go 倡导 组合优于继承
  • 如果只是复用代码,直接用结构体嵌入即可。
  • 如果需要类似“虚函数”的效果,可以考虑:
    • 简单情况:函数指针回调
    • 框架式逻辑:Self Reference 技巧
  • 如果子类逻辑与父类差别很大,不妨直接 重写方法,更清晰。