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

继续昨天的:[翻译]Go编程语言,或者:为什么除了它,其他类C语言都是垃圾(1)
总算切入正题,开始说 Go 了。

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

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

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

进入 Go 的世界

概述

在第一次听说 Google 的新编程语言时,我有一些怀疑。于是忽略了那条新闻。在那之后,下一代新的、伟大的语言就充满了各个地方。其中一些享受于璀璨夺目,然后就暗淡消沉;有一些走上了邪路;还有一些截止现在已经准备了十年的发布

过了一段时间,我再次与它相遇。这回我凑近看了看。有一个我之前没有留意的事情:其中一个发明者是因 Unix 和 Plan9 而闻名遐迩的 Ken Thompson, 而他同样也间接参与了 C。那么如果新的编程语言是由曾经参与过大型主机时代的领军任务人物设计的,那么可能还是有料的。

因此 Go 到底可以给我什么是 Perl、Python 和 JavaScript 无法做到的?它能做什么是它们之前可以做到的?是什么使得它与其他失败或者有限的成功的语言不同的?它会在以后的 30 年里称雄吗?以及最重要的:它能满足我的需求吗?

第一次接触

Go 的一个重要事实是它面向的用户群。它是按照系统编程语言设计的,所以瞄准的是底层软件,甚至是 OS 的内核。因此,由于复杂度和与硬件结构的吻合度,高层次的构架可能会缺失。有趣的是,大部分便利却仍然存在。

接下来了解到的是,这是一个 C 血统的语言,因此使用 US 键盘布局让花括号容易按。但是当阅读例子代码时,它看起来并没有想象中的那么像 C 系的。更少的括号,看不到分号,几乎没有变量声明,至少第一看看上去没有。语法相当轻量,有着明显不同的保留字和控制结构,但是还是很好懂的。

与 C 不同的要点

对于那些熟悉 C/C++ 的人,来一起对那些不同做一个快速的对比:

没有分号!

不是玩笑!当然,实际上是有分号的,但是被省略了。这就像 JavaScript,有简单的规则让语法分析器在正确的行结束处添加分号。而这个规则相当简单,所以它很容易在源码处理工具中实现。

OTBS(译注:K&R样式)

接下来是“真正的邪端”:Go 定义了声明规范和 One True Bracing 样式(译注:就是 K&R 的代码样式)。而 RMS(译注:Richard Stallman)可能不会对此感到高兴。 以至于提供了 gofmt,一个用规范格式化代码的工具。当然,Java 开发者已经这么做了,当他们停留在一些导致脑瘫的情况下(缩进为2?SRSLY(译注:我真不知道这是什么,另一种代码格式?)?)。Go 代码的格式化可以归纳为:

用 tab 缩进(这让用户可以按照他舒服的空格数量设置编辑器)
花括号与其隶属的控制语句在同一行
折行不能以反花括号或标识符结束,例如操作符留在上一行,而不是新行的开始。

简单的分号插入原则的结果其实导致了第三点。我希望能有办法绕过它,因为我喜欢连接的操作符在折行的开始,来强调发生了什么,而不是将其隐藏在一堆东西的后面(译注:开始我也觉得这点有点恶心,不过习惯了之后,就会先从后面看起了)。

不过除了这个,其他多数都还是非常明智的。许多人已经深入的解释了这个。要点是更少视觉干扰的可读性。就像 Python 那样,只是我觉得 Python 看起来过于缺少可视的提示。缩进并不总是足够清晰的,所以仍然保留心爱的花括号吧。

强制花括号

关于花括号,在 if 和 loop 上是没有无花括号的模式的,我认为这令人遗憾。代码样式纯粹论者可能会喜欢它。但是最终,我并不在意这个。对于确实短的语句,我可以这么做

if cur < min { min = cur }

双选项

前一个要点的重要的技术原因之一是控制语句可以接收双选项。只有 Perl 6 在没有花括号的情况下会尝试解析,而我们都知道 Perl 的语法解析器之前有多么复杂(显而易见,现在也是)。所以,这实际上是一个关于什么是强制的,什么不是的抉择。由于在多数情况下都需要用到花括号,所以这是非常明智的。这阅读起来不同寻常,必须适应有这样的划分,但是一旦习惯了它,Go 代码感觉比 C 代码更轻量一些。

明确命名类型、函数和变量

为了说明这个,需要用到保留字 type、func 和 var。这很清晰,会有一个很好的阅读“流”。技术上的原因,接着读吧。

隐式定义,自动设定类型

变量总是静态的指定类型,就像 C 那样。但看起来并不是这样。如果你不指定类型,类型代由赋值语句指定。利用新的定义和初始化操作符,甚至可以连同声明一起省略:

foo := Bar(1,2,3)

这定义了叫做 foo 的变量,并向其赋 Bar 类型的值。这个作用与

var foo Bar = Bar(1,2,3)

完全相同。

这并不是在介绍动态类型,这不允许改变已经定义了的变量的类型,也没有将变量定义的需要移除,这不允许将变量定义两次。这和之前完全一样,语义上,在语法上轻量很多。感觉像特定的脚本语言,但是仍然有静态类型的好处。

倒序的变量定义

在 Go 中,变量的类型和函数返回值的类型在名字之后,要用

var amount int

代替

int amount;

这补充了之前可以省略显示的类型定义的特色,提供了更加完整的概念。

没有运算的指针

仍然有指针,只是它们现在仅作为值的引用。所有的对象是值类型,因此赋值会复制整个对象。指针提供了在 Java 中作为默认的引用语义。但是没有指针运算。你被强制像数组这样的方式来访问,这为安全信任问题的解决带来了回旋的余地。是的,先生,我们都有边界检查!

因此,指针不再是算法的核心。它们仅服务于一个目的,引用 vs. 值的语义。这使得无法通过指针访问未引用的成员的实现变得简单。foo.Bar() 同时工作于指针和值的情况下。

垃圾回收

了之前的要点大量描述了是否能传递值以及所有的指针是否都是安全的概念,以及对于每个和所有变量来说是否能够获取地址,就像你在 C 和 C++ 中不能做的。

然后:内存管理是由垃圾回收处理的。最终!通过集成 boehm-gc 这平衡的类垃圾回收使得可以安全的传递一个指针到局部变量,或者获取一个临时变量的地址,这都将正常工作!嘢!

对那些没有与时俱进的研究垃圾回收的人来说,可能会不仅仅对 GC 解决了若干使用 malloc/free 的错误导致的安全性感兴趣,可能还关注 GC 是否足够快。一个适当的垃圾回收实际上可以比手工内存管理更快,通过恰当时间的延迟记录,或通过重用未使用的对象完全避免记录。一些更加高级的 GC 将这些联合使用,使得内存更加有效率,缓存使用内存减少碎片的产生。这不再是 C64 了。

现阶段 Go 的 GC 相当简单,但是一个更加高级的实现正在进行中。在大多数情况下,GC 是胜利者,但是它当然也有局限。在一些临界情况下,可以使用预分配对象的数组。

变长数组

这与那些在运行时就确定了大小,并不再变化的数组无关。这是那些你可能不得不使用 realloc() 的东西。数组总是有固定的大小,这是来自 GNU-扩展 C 的一个小的向下兼容。但是,作为代替,就有了 slice。

slice 看起来和感觉起来都像数组,但实际上它们只是映射到一个固定大小的原始数组的一个子范围。由于有垃圾回收,slice 可以引用到匿名数组。这是获得动态大小的数组的真相。有内建函数用于重新指定 slice 的大小,并且当底层数组过小时进行替换,因此在其上编写一个向量类也是很容易的。

反射

Go 支持反射,也就是说,可以查看任何类型,并获取其类型信息、结构、方法等等。Java 和 C++ RTTI 支持这个,但是 C 里面没有。

不定大小常量

常量可以不指定类型,甚至不指定大小。数字常量可以用在其数字是合法的任何上下文中,并且会使用相关的数据类型指定的精度。常量表达式用完整精度进行计算,然后当需要时截断到目标类型的精度。没有类似 C 中的常量大小的后缀。

错误处理

Go 没有异常。等等——是严肃的吗?这藐视了常识中接受的关于安全和稳定的程序的常规!这不是巨大的倒行逆施吗?

实际上不是。诚实的说,什么时候你很好的检查并处理了异常?大多数时间,它们是这样的:

try {
    openSomeFile();
    doSomeWorkOnTheData();
    yadda...
    yadda...
    yadda...
    closeItAgain();
} catch (IOException foo) {
    alert("Something failed, but there's not enough time to do proper error handling");
}

罗嗦,增加一个缩进级别而没有任何好处,也没有解决问题的根源,开发人员的懒惰。如果想要单独处理每一个调用的错误,如此罗嗦的代码使得更加不清晰。

因此总是可以在复杂的代码过程上回到学校中按部就班的错误处理。返回值使用了许多年了,但是 Go 有多值返回这个很好的现代功能。所以忘记会导致脑瘫的 atoi() 吧,我们有附带的标记。为了那些在乎的人们。为了那些不在乎的人们,那些即使 Java 尝试强制要求错误处理,也都不在乎的人们。

这里有 panic。它用于“可能无法继续”类型的错误。严重的初始化错误, 威胁正常的数据和运算的情况,诸如这类问题。无法恢复的错误,简单来说。语言的运行时环境也可能产生 panic,例如数组越界。

当然,这将我们带回到清理使用过的资源这个问题了。为了这个,defer 语句出现了,并且它相当优美,让错误处理属于它应在的地方,正确的对待问题:

handle, err := openSomeFile()
if err != nil { return nil, err }
defer closeSomeFile(handle)
return happilyDoWork()

defer 非常像 Java 中的 finally 分支,但它看起来更像修饰函数调用的。它确保了 closeSomeFile 被调用,无论函数是如何退出的。另一方面,当成功时也可以越过关闭它。很少的代码冗余,简单并且明了的错误处理。多个 defer 也是允许的,可以按照 LIFO 顺序正确执行。

对于那些在 panic 后想要继续的情况下,有 recover。panic 和 recover 一起就可以实现通常意义上的异常。由于它们会引起程序流程的混乱,官方建议非致命 panic 不应该越过包的边界。对于错误处理没有核心思想,所以两种情况最好都处理好,并且应当手工选择对于任务来说不复杂的那个方法。

控制结构

感谢 range 保留字使得仅有的 for 循环可以像 foreach 那样工作。在特殊情况下使用 for,这种语法使得更轻量(看上面),这让我感觉很好。或者更好的:可以将标签放在嵌套的循环中,通过它们可以一次跳出多层。完美了!

同样可以用 goto 拿自己来打靶。好吧,开个玩笑,那些邪恶的东西只是简单禁止。但是如果谁愿意,为了一些简单的理由也可以这么做。

这些仅仅是一些概览。还有一些细节,等会会涉及到,但是所有一切对于 C 来说都是重大的改进。在许多个快乐的夜晚编写没有对象的过程代码就已经足够了,不是吗?
———————–翻译分割线———————–
一个德国人,用了这么多英文生僻的词语,这么多生僻的用法,这么多俚语和缩略语,他是怎么做到的?怎么做到的呢?求秘籍啊!求秘籍啊!

今天就到这里吧……还有好多,好多内容没完成。

这位德国人,有着他们血统的认真与执着。好吧,他大段大段的增加了内容。我也跟随补充了“错误处理”和“控制结构”。

Join the Conversation

10 Comments

  1. 一个德国人,用了这么多英文生僻的词语,这么多生僻的用法,这么多俚语和缩略语,他是怎么做到的?怎么做到的呢?求秘籍啊!求秘籍啊!
    —————————————————————————————————————————————–
    你是怎么做得到的,翻译速度这么快,求秘籍啊!求秘籍啊!

Leave a comment

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