[翻译]Go编程语言,或者:为什么除了它,其他类C语言都是垃圾(3)

继续前文的翻译。更进一步的对 Go 进行了介绍。一个德国人,用英文写了如此的长篇大论,这是一种什么样的国际主义精神……

———————–翻译分割线———————–

Go编程语言,或者:为什么除了它,其他类C语言都是垃圾

[翻译]Go编程语言,或者:为什么除了它,其他类C语言都是垃圾(1)

[翻译]Go编程语言,或者:为什么除了它,其他类C语言都是垃圾(2)

扩展

Go 真正强大的在于到现在为止,那些无法在 C、C++或者其他上面提到的任何语言中找到对应的地方。这些才是真正让 Go 光彩夺目的:

基于类型的对象 vs. 封装

没有类。类型和类型上的方法相互并无依赖。可以在任何类型上定义方法,也可以定义任何类型为一个新的类型,这和 C 中的 typedef 相似。与 C 或 C++ 的不同之处在于新的命名的类型有其自己的方法集合,而主要类型(那些作为基础的)的也会有方法集合:

type Handle int64

func (this Handle) String() string {
    return "This is a handle object that cannot be represented as String."
}

var global Handle

在这个例子中,global.String() 可以被调用。更进一步,我们获得了一个没有虚方法的对象系统。这没有任何运行时的麻烦,只是语法糖而已。

鸭子类型(译注:参考维基百科) vs. 多态

类型定义不能让两个独立的类型看起来相似。独立的类型就是独立的类型,在类型严格的语言中不允许创建一个通用类型实现多态。在大多数语言中有一个很流行的例子,就是值转换表达它的字符串的规则。Handle 类型用 Go 的规则定义了这样一个方法,但是没有展示如何让任意的类型都有这样一个 String 方法。

C++ 使用接口(可能利用虚基类)并且重载运算符来实现这个。Java 的 toString 是根类的一部分,因此会被继承,而其他调用规则都是根据接口来表达的。

Go 使用接口有些特别。不像 Java,它无须定义给定的类型匹配某个接口。如果可行,那么就自动作为那个接口:

type Stringer interface {
    String() string
}

这就是所有需要的。自动的,Handle 对象现在可以作为 Stringer 对象使用。如果它走起来像鸭子,叫起来像鸭子,并且看起来像鸭子,那么从任何实际用途出发,它就是鸭子。现在最棒的部分:它可以动态的工作。无须导入,甚至开发者无须知道接口的定义。

当类型作为接口使用时,运行时环境为了得到接口的运行时的反射的能力,构建了一个函数指针的表格。这样就会有一些运行时的开销。然而这进行了优化,所以只有很小的损失。接口表格只在真正使用的时候才进行计算,对于每个类型来说只计算一次。如果编译时能够确定实际的类型,就完全避免在运行时处理。方法调度应当比 Apple(已经相当酷了)的 Objective C 的调度略快。

类型嵌入 vs. 继承

类型的作用跟类类似,但是由于没有继承的层级,它们实际上是不同的。在之前的例子中,Handle 并没有从 int64 中继承任何方法。可以通过在声明体中包含基础数据类型,来定义一个类型结构实现类似继承的东西:(无耻的从“Effective Go”中窃取的例子)

type ReadWriter struct {
    *bufio.Reader    
    *bufio.Writer
}

bufio.Reader 和 bufio.Writer 有的所有方法这个类型都有。冲突用一个很简单的规则解决。毕竟这不是多重继承!每个基础类型都作为联合类型中的一个独立的数据对象存在,而来自子类型的每个方法只能看见它所拥有的对象。这个方法,就可获得良好实用的行为,而不存在类的多重继承导致的那些麻烦。抛开所有概念——这多多少少又是一个语法糖,无须运行时开销就让代码更加有表现力。

这同样工作于接口。联合类型匹配全部组成类型匹配的接口。解决引用的方法歧义的规则非常简单,而无法预测的情况被简单的禁止了。联合类型可以自由的按照需要重写组成的方法。

可见控制

开发中的主要单元是包。一个或者多个文件实现一个包,并且可以控制从包外访问的可见情况。这里没有复杂的可见系统,仅仅是可见或不可见。这是由排版规则控制的:公共名字由大写字母开始,私有名字用小写字母。这对于所有命名都有效。

这是个相当务实的方案。不像类型系统,这在竞争中相当有力,这里 Go 占领了中土:多数脚本语言完全不关心可见性或者依赖自觉规定,而学校式的 C 系语言有访问控制的细节。同样,这对于 Go 的对象模型也是一件好事。由于没有类继承并且嵌入对象是完全结合的,就没有需要有访问保护规则。下面会看到这是如何在实践中工作的。

没有构造函数

对象系统没有特别的构造函数。这里有一个零值的概念,例如用零初始化类型的所有字段。这要求在编写代码时,零值对于合法的“空”对象处理是有意义的。如果无法做到,就提供作为包函数的构造函数。

Goroutine 和 Channel

对于这种通用编程语言来说,这是最不寻常的功能。Goroutine 可以认为是极为轻量的一种线程。Go 运行时将这些映射为 pth 形式的多任务协作伪线程或真正的操作系统线程,前者拥有较低的开销,后者通过线程得到了非阻塞的行为,因此得到了两者的最优。

Channel 是有类型的缓存或无缓存的消息队列。一个简单的实现,真的。从一边装填入对象,从另一边取出来。你不得不在并行算法中做的许多事情都不再需要了。

Go 让 Goroutine 作为一等公民是一大进步,许多算法都可以用其作为核心。同段的栈使得每个线程的最小栈使用都比较低,通过模拟线程得到性能上的优势,除非使用阻塞的系统调用,也就是仅比普通函数调用的开销略多一点。

综上所述,Go 有真正的终极武器,Goroutine 可以用在许多地方,而不是标准的并发算法。Python 的 generator 函数可以作为模型,或者一个自定义的自由对象内存管理表。参阅在线文档,如此简单的并发实现是一件神奇的事情。

顺便说一下,通过设置环境变量,你可以告诉 Go 运行时希望使用多少个 CPU,这样 Goroutine 将会在开始的时候映射若干个原生线程(默认如果没有阻塞的系统调用,就不开启任何其他线程)。

缺陷

没有系统是完美的。Go 有一些缺陷,这里的列表列出了现在我遇到的:

二进制大小/运行时依赖

基本的 Go 二进制是静态链接,如果编译不包括调试信息,大约 750k。这和类似的 C 程序大小相同。我已经用 Go 主页上的“比较树”例子进行了测试,将其同类似结构的我的 C 实现进行了比较。

gccgo 可以编译动态链接的执行文件,但是 libc 在所有系统上,并且通常不需要考虑依赖,而 libgo 有大约 8MB 的额外的包。作为比较:libstdc++ 小于 1 MB,libc 小于 2MB。公平的说,它们相比 Go 的标准库要少做很多工作。然而,还是有很大差异以及有依赖问题。

6g/8g,原生的 Go 编译器,生成类似的执行文件,但是并不依赖 libc,它们是真正独立的。却也无法实现运行时的动态连接。

这也同样涉及小系统。在我的旁边是古老的 16MB 奔腾-100 笔记本,运行着 X 和 JWM 桌面,正愉快的播放我的音乐收藏。它甚至有 5MB 的内存用于磁盘缓冲。有可能用 Go 编写这样的系统吗?

不公平的权利

这个语言在许多方面存在特权。例如,特别的 make() 函数所做的工作,不能用用户的代码对其进行扩展。这在开始并不像看起来的那么糟糕,例如可以写一些与 make() 作用相同的代码,仅仅是没有办法对这个语言的构造进行加强。同样的问题在其他可能需要扩展的调用和保留字上都存在,例如 range。你或多或少被强制使用 goroutine 和 channel 扩展一个高级的出来。

我不确定这真得是个问题。假设 map、slice、goroutine 和 channel 是可选的实现,这个限制带来的冲击就不存在了。它并不损害代码的清晰程度和可读性,但是如果用过“可以模仿任何东西”的语言如 Perl 或者 Python,就会感觉有点不公平。

没有重载

重载是许多语义歧义的根源。有很好的理由让它滚蛋。但是,同时重载,尤其是运算符重载,是如此方便和可读的,因此我很想念它。Go 没有自动的类型转换,因此事情不会像在 C++ 中那样令人毛骨悚然。

作为例子,重载可能用于的地方,设想大数库、(数字)向量、矩阵,或者有限范文的数据类型。当处理夸平台数据交换,可以对特殊数据类型修改数学语义,会是一个巨大的胜利。对于处理古董机的数据,可以有补充的数字类型,例如,用完全模拟目标平台的运算,来代替依赖当前平台语义上的相同。

有限的鸭子类型

不幸的是,鸭子类型是不完全的。设想一个像这样的接口:

type Arithmetic interface {
    Add(other int) Arithmetic
}

函数 Add 的参数和返回值将会限制自动的类型化。一个有方法 func (this MyObj) Add(other int) MyObj 的对象不匹配 Arithmetic。有许多类似这样的例子,而它们中的一部分很难决定到底应该用鸭子类型覆盖它们,或者当前的规则更好。你可能落入许多不太明显的问题中,因此这是个“保持简单可能会更好”的例子,但我总是觉得不够方便。

Russ Cox,Go 核心的作者之一,说明:

这不能工作的原因是 MyOjb 的内存布局与 Arithmetic 的内存布局不同。即便是内存布局吻合的其他语言也在纠结于此。Go 只是说了“不”。

我猜测需要定义 func (this MyObj) Add(other int) Arithmetic 来代替。妥协的方案带来的好处是编译器和生成的机器码更加简单。

指针 vs. 值

我不确定对这个指针/值的事情到底高兴不高兴。Java 的所有都是引用的语义更简单。C++ 引用 vs. 值的语法同样也是不错的。一个可能的方面是你获得了更多对内存布局和使用结构的控制,特别是当包含其他结构时,而值 vs. 引用语义在函数调用的时候很清晰,C++ 却难以预料。

顺便说一下,map 和 slice 是引用类型。在最初的时候,我对它们感到烦恼,但是你可以构造有着类似行为的自己的对象:包含(私有)指针的结构体,这或多或少是 map 和 slice 做的事情。现在只剩下如果有办法钩挂到其 […] 语法中去的话……

逻辑上下文(译注:三元运算符,还记得吗?)

逻辑上下文提供了许多简化代码的可能。不幸的是,虽然 !pointer 还是很清晰的,但指针仍然必须同 nil 相比较。更进一步说,从现在起不再有指针运算。参考上面的期望的第十条。在让代码工整同时有更短。

给每个类型一个零值的观念,这微小的弥补了逻辑上下文的不足。
———————–翻译分割线———————–
发现德国佬普拉普拉的又修补了 review,同时加了一大堆内容。心都凉了,这个坑越挖越深了……

Join the Conversation

4 Comments

Leave a comment

Your email address will not be published. Required fields are marked *