Go 到底支不支持面向对象?答案有点绕:支持,但又不是传统意义上的支持。它没有 class 关键字,也没有 Java 那样的继承语法,能拿出来的"只有"结构体(struct)。不过结构体可以挂方法,再配合模块,甚至能凑出类似构造函数的东西。

打开网易新闻 查看精彩图片

先看一个最基础的例子。定义一个 Animal 结构体,字段是 name、color、age,全部小写——这意味着它们都是私有的。再写一个 NewAnimal 函数返回结构体指针,充当构造器的角色。

接着给 Animal 挂上 Name()、Color()、Age() 三个方法,只负责返回值,不提供修改入口。这样一来,这些字段本质上就成了只读属性。调用 NewAnimal("Panchito", "white", 18),打印出来就是 Panchito is 18 years old.

这里有个细节值得说清楚:返回结构体指针是安全的,它表示对象按引用传递,但并不是在返回函数栈上的指针。另外 Go 不区分"用指针访问对象"和"用对象本身访问",两种情况下都用点号('.')选择成员,比如 cat.Name()。

方法的写法也和别家不同。方法不塞进结构体内部,而是写在结构体后面,把接收者放在函数名之前。接收者从不叫 this 或 self,一般用结构体名的缩写。

Go 里没有 toString() 的等价物,但有 Stringer 接口,它要求结构体提供一个名为 String() 的方法。给 Animal 加上 String() 之后,main 函数里直接 fmt.Print(cat) 就行,输出变成:Panchito is white and 18 years old.

面向对象的三大特征是封装、继承、多态。封装已经有了:方法和属性可以放进同一个结构体,还能控制公开或私有(但没有 protected 这种东西)。接下来看继承。

Go 没有 extends 这样的专用语法,但可以把结构体 A 嵌入结构体 B,A 的方法就会"提升"为 B 的方法。定义一个 Cat,嵌入 Animal,再加一个 noise 字段,配上 NewCat 构造函数和 Noise() 方法,猫就拥有了动物的全部特征,外加自己的叫声。

因为 Animal 的方法提升到了 Cat,fmt.Print(cat) 的输出保持不变,仍是 Panchito is white and 18 years old.,后面再补一行 Purrr... 打印叫声。所以,继承这件事,Go 算是有了。

那多态呢?能不能放心地调用 foo(),确保执行的是对象自己的 foo()?可以。如果 Animal 和 Cat 有同名方法,后者会遮蔽前者。给 Cat 写一个自己的 String(),输出就变成:Panchito is white and 18 years old. Says: Purrr....

当然,和 Java、C# 的习惯相比仍有落差。比如不能让一个 *Animal 指针指向一个 Cat 对象:

  • var cat *Animal = NewCat(...)
  • 这段代码编译不过
  • 编译器报错:cannot use NewCat(...) (type *Cat) as type *Animal in assignment

这个问题怎么解决,要留到后面的篇目再说。回到最初的问题:没有 class,没有继承关键字,Go 靠结构体、方法、嵌入和接口,把面向对象的三件套凑了个七七八八。它不照搬任何一门语言的样子,但该有的能力,一样没落下。