Learning Go: Multiple Inheritance of C++ v.s. Struct Embedding of Go

Go语言提供了一种神妙的功能--embedding,用来”合并几个部件的功能”。什么叫”合并几个部件的功能呢”?让我们从比较熟悉的面向对象的角度出发,来说一说。

假设我们有两个 classes:

class Painter {
 public:
  void Paint() { ... }
};
class Hacker {
 public:
  void Hack() { ... }
};

假设现在我们需要一个新的class PaulGraham,它既有Painter的技能(Paint),也有Hacker的技能(Hack)。我们应该如何来写 PaulGraham 这个class呢? 一个办法是用 embedding pattern:

class PaulGraham {
 public:
  void Paint() { painter_.Paint(); }
  void Hack()  { hacker_.Hack(); }
 private:
  Painter painter_;
  Hacker  hacker_;
}

从这个例子可见:在C++里做embedding是挺麻烦的:我们需要給 PaulGraham 定义很多 methods,要覆盖 Painter 和 Hacker 的所有 methods,而且每个 methods 的定义的形式都很呆板--浪费程序员的时间。

当然,C++也提供了另外一套办法来定义 PaulGraham,这就是多继承(multiple inheritance)。如果用多继承,那么PaulGraham的定义就简单很多了:

class PaulGraham : public Painter, public Hacker {};

看着很简单吧?这是因为C++编译器替你做了前一个例子中的embedding的工作。

但是很多介绍C++的资料都会提醒你--不要用多继承,而是尽量用embedding(比如Google C++ Code Style)。大概的原因是:编译器替程序员做了太多实际工作之后,程序员容易爽翻了,忘记了简单的代码下隐藏的复杂性。

Go语言的设计者估计也受到了这种思路的影响--他们搞了一个embedding机制,但是不需要程序员写太多形式呆板的代码。上面的例子如果用Go语言来写,是这样的:

type Painter struct {}
func (p Painter) Paint() { fmt.Println("Painting ... Done."); }

type Hacker struct {}
func (h Hacker) Hack() { fmt.Println("Hacking ... Done."); }

type PaulGraham struct {
	Painter
	Hacker
}

请看看,PaulGraham的定义只有三行--很简单吧。实际上就是定义了两个没有名字(变量名)的data members

实际上,定义两个指针变量也是可以的:

type PaulGraham struct {
	*Painter
	*Hacker
}

爱钻牛角尖的朋友(比如我)可能会说了:C++的书上不建议我们用multiple inheritance的时候,经常举一个复杂的例子: 如果 Painter 和 Hacker 都继承自 Person,那么 PaulGraham 里不是被embed了两个 Person 对象了吗?如果Person支持一个method叫做Live,那么 PaulGraham 的 Live 函数是来自哪一个 Person 对象呢?

这个问题的答案是: C++编译器和Go编译器都会晕菜(除非程序员告诉编译器),然后报错。你可以写一个C++程序来试试;我写了一个Go程序如下:

package main
import "fmt"

type Person struct {}
func (p Person) Live() { fmt.Println("Living ... go on"); }

type Painter struct { Person }
func (p Painter) Paint() { fmt.Println("Painting ... Done."); }

type Hacker struct { Person }
func (h Hacker) Hack() { fmt.Println("Hacking ... Done."); }

type PaulGraham struct {
	Painter
	Hacker }

func main() {
	painter := Painter{}
	hacker := Hacker{}
	paul := PaulGraham{painter, hacker}
	paul.Paint();
	paul.Hack();
	paul.Live();  // ambiguous DOT reference PaulGraham.Live!
}

Go编译器会晕死在main函数的最后一行。

这个帖子只是想说C++和Go都可以实现多继承,由此可见Go作为一种system programming language的强大的表达能力。但是多继承确实是不应该常用的。如果按照Google C++ Code Style的建议:

Multiple inheritance is allowed only when all superclasses, with the possible exception of the first one, are pure interfaces. In order to ensure that they remain pure interfaces, they must end with the Interface suffix.

上面例子,应该修改成 PaulGraham 派生自 Person, Painter和Hacker。