Go 和 Rust 都舍弃(或极度弱化)经典的“实现继承”(implementation inheritance),核心原因高度一致:继承在实际工程中带来的问题往往大于它解决的问题,尤其是当项目规模变大、团队变大、时间变长之后。
下面把两门语言的动机和取舍放在一起对比:
| 维度 | Go 的做法与动机 | Rust 的做法与动机 | 共同指向的问题(为什么现代语言集体反感) |
|---|---|---|---|
| 是否有 class | 完全没有 class | 没有 class,只有 struct + enum + trait | class 通常捆绑了数据 + 行为 + 继承,过于重量级 |
| 是否有实现继承 | 完全没有(嵌入只是字段提升,不是继承) | 完全没有(早期设计阶段移除过 layout 继承提案) | 是最核心的共同点 |
| 多态怎么实现 | interface + 隐式实现 | trait + 显式实现(impl Trait for Type) | 都选择“鸭子类型”或“类型可以实现多个行为”的方式 |
| 代码复用主要靠什么 | 组合 + 接口 + 嵌入 | 组合 + Trait + 泛型 | Composition over Inheritance |
| 父类字段布局问题 | 不存在(因为没有继承) | 不存在,但如果允许继承会破坏内存安全与零成本抽象 | 继承会让子类布局依赖父类,破坏 Rust 的内存模型 |
| 脆弱基类问题 | 不存在 | 不存在 | 修改基类 → 所有子类可能都坏掉 |
| 菱形继承/多重继承问题 | 不存在 | 不存在(trait 可以多实现,但没有数据继承) | 多继承的致命伤(C++ 最典型) |
| 方法查找/分派复杂度 | 非常简单(方法是包级函数,接收者决定) | 静态分派(泛型)或动态分派(trait object),但没有 vtable 继承链 | 继承链越深,方法解析越复杂 |
| 对大型项目/长期维护友好度 | 高(类型之间没有血缘关系,改动影响范围可控) | 极高(所有权+借用+trait 让耦合更可控) | 这是 Go/Rust 设计者最在意的点 |
| 对性能/零成本抽象影响 | 嵌入是零成本的 | trait + 泛型是零成本的,trait object 才有运行时开销 | 继承的虚函数表通常有运行时成本 |
为什么继承被认为“害处大于好处”?(最常见的工程痛点)
- 脆弱基类问题(Fragile Base Class Problem)
基类加/改/删一个 protected 方法或字段 → 所有子类可能都要重新编译/出问题 - 紧耦合 & 隐式依赖
子类严重依赖父类的实现细节,而不是只依赖接口 → 父类一改,子类全炸 - 难以理解的深继承链
第七层子类调用一个方法,到底执行的是哪一层的实现?需要翻很多文件 - 多重/菱形继承的灾难(C++ 经典地雷)
- 测试/ mock 困难
继承的类很难替换父类行为(除非重写所有方法) - Rust 特有:破坏内存安全与零开销抽象
如果允许字段继承,子类的内存布局就依赖父类 → 与借用检查器、Drop 顺序、移动语义严重冲突
Go 和 Rust 分别用什么替代了继承?
| 想实现的效果 | Go 的常用写法 | Rust 的常用写法 |
|---|---|---|
| 代码复用 | 嵌入结构体(embedding) | 组合 + 写转发方法,或用 derive 宏 |
| “我是某种东西” | 实现接口(隐式) | impl Trait for Type |
| 多态 | 接口值(接口是胖指针) | Trait Object(&dyn Trait / Box) |
| 默认实现 | 接口没有默认实现 → 手动写 | trait 可以提供 default 方法 |
| 模板方法模式 | 组合 + 接口回调 | trait + 泛型 + 默认实现 |
| 抽象类 | 没有 → 用接口 + 空结构体技巧 | 没有 → 用 trait + 要求实现某些方法 |
一句话总结现代语言(Go、Rust、Zig、Nim 等)的共识:
继承是把“是一种”(is-a)关系和“复用实现”强行捆绑在一起,而这两件事其实应该分开处理。
- “是一种” → 用 接口 / trait 表示(行为契约)
- “复用实现” → 用 组合 + 委托 更灵活、更可控
所以 Go 和 Rust 不是“忘记”做继承,而是故意不做,它们认为这是对程序员长期友好的选择。
你目前更常用哪一门?在实际项目中遇到过特别想用继承但被语言限制住的场景吗?可以聊聊具体例子~