Web开发

注册

 

发新话题 回复该主题

那些Go语言发展历史上的重大决策 [复制链接]

1#
拼多多运营求职招聘交流QQ群 http://liangssw.com/bozhu/12719.html

Go是年末由谷歌创立的一种程序设计语言,年11月以开源形式发行。自那以后,Go就作为一个公共项目运作,有成千上万的个人和几十家公司作出贡献。Go已经成为一种很受欢迎的语言,用于构建云计算基础设施:Linux容器管理器Docker和容器部署系统Kubernetes是由Go开发的一种核心云计算技术。现如今,Go已经成为了各大云计算提供商的重要基础设施的基础,也是云原生计算基金会托管的大多数项目的实现语言。

有许多理由让早期使用者对Go感兴趣。一种用于构建系统的垃圾收集、静态编译的语言是不寻常的。Go提供的并行性和并发性的原生支持,使其能够充分发挥当时正在成为主流的多核机器的优势。自带的二进制文件和简单的交叉编译使部署变得更加容易。当然,谷歌这个名称也是一大亮点。

但是为什么用户会留下来?为什么Go在很多其他语言项目还没有开发出来的时候,它就变得如此流行了呢?我们认为,语言本身只是答案的一小部分。完整的故事应该包括整个Go环境:库、工具、约定和软件工程的整体方法,这些都支持用该语言编程。所以,在语言设计方面,最关键的决策是让Go能够更好地适应大型软件工程,并且能够吸引有相同想法的开发人员。

在本文中,我们将会回顾那些我们认为对Go的成功负有最大责任的设计决策,并探讨这些设计决策如何不仅适用于语言,而且适用于更广泛的环境。很难将具体决策中的贡献分开,因此本文不应被视为一种科学的分析,而是一种对Go十多年来的经验和对用户反馈作出的最好的诠释。

起源

Go的诞生源于谷歌构建了大规模分布式系统,在一个由成千上万的软件工程师共享的大型代码库中工作。我们希望为这种环境设计的语言和工具能够应对公司和整个行业所面临的挑战。随着开发工作的开展和生产系统的大量部署,这些都带来了一些挑战。

开发规模。在开发方面,谷歌在年有大约名活跃的用户在一个单一的、共享的、多语言(C++、Java、Python)的代码库中工作。单一的代码库使它很容易修复,例如,内存分配器中的问题会让主Web服务器变慢。但是在使用库的时候,由于很难找到一个包的所有依赖关系,所以很容易在不知不觉中破坏了一个以前未知的客户端。

另外,在我们使用的现有语言中,导入一个库可能会导致编译器递归加载所有导入的库。在年的一次C++编译中,我们观察到,(在#include处理后)传递一组总共4.2MB的文件时,编译器读取了超过8GB的数据,在一个已经很大的程序上,扩展系数几乎达到。如果为编译一个给定的源文件而读取的头文件的数量随着源树线性增长,那么整个源树的编译成本就会呈平方增长。

为了弥补速度的减慢,我们开始研究一个新的、大规模并行和可缓存的编译系统,它最终成为开源的Bazel编译系统。我们认为,光靠语言本身是远远不够的。

生产规模。在生产方面,谷歌运行的是规模非常庞大的系统。例如,在年3月,Sawzall日志分析系统的一个拥有块CPU的集群处理了2.8PB的数据。年8月,谷歌的个Big-table服务集群由个独立的Tablet服务器组成,其中一组个服务器每秒处理万个请求。

不过,像业界其他公司一样,谷歌也在致力于编写高效率的程序,以便充分发挥多核系统的优势。我们的很多系统都必须在一台机器上运行同一个二进制文件的多个副本,这是由于现有的多线程支持繁琐且性能低下。庞大的、固定大小的线程栈,重量级的栈开关,以及用于创建新线程和管理它们之间的交互的笨拙语法,都使得使用多核系统变得更加困难。但是显然,在服务器中,内核的数量只会越来越多。

我们还认为,语言自身能够提供易于使用的轻量级的并发性原语。我们也在这些额外的内核中看到了一个机会:垃圾收集器可以在一个专用的内核上与主程序并行地运行,这样可以减少它的延迟。

我们想知道,为应对这些挑战而设计的语言可能会是什么样子的,答案就是Go。Go的流行,一定程度上是因为所有的科技行业都要面对这样的挑战。云计算提供商使得最小型企业也可以将目标锁定在大规模的生产部署上。尽管大部分公司没有数千名雇员编写代码,但是如今几乎每个公司都依靠着数以千计的程序员完成的大量开源基础设施。

本文的其余部分将探讨具体的设计决定如何解决这些开发和生产的扩展目标。我们从核心语言本身开始,向外扩展到周围的环境。我们不打算全面地介绍这门语言。关于这一点,可以参阅Go语言规范或者《Go编程语言》(TheGoProgrammingLanguage)之类的书籍。

一个Go程序是由一个或多个可导入的包组成的,每个包都包含一个或多个文件。图1中的Web服务器展示很多有关Go的包系统设计的重要细节:

图1:GoWeb服务器

该程序启动了一个本地的Web服务器(第9行),它通过调用hello函数来处理每个请求,hello函数用消息“hello,world”(第14行)进行响应。

与许多语言相同,一个包使用明确的import语句导入另一个包(第3-6行),但与C++的文本#include机制不同。不过,与大多数语言不同的是,Go安排每个import只读取一个文件。例如,fmt包的公共API引用了io包的类型:fmt.Fprintf的第一个参数是io.Writer类型的接口值。在大多数语言中,处理fmt的import的编译器也会加载所有的io来理解fmt的定义,这可能又需要加载额外的包来理解所有io的定义。一条import语句可能最终要处理几十甚至几百个包。

Go采用与Modula-2相似的方式,将编译后的fmt包的元数据包含了了解其自身依赖关系所需的一切,例如io.Writer的定义,从而避免了这种工作。因此,import"fmt"的编译只读取一个完全描述fmt及其依赖关系的文件。此外,在编译fmt时,可以一次性实现这种扁平化,这样就可以避免每次导入时的多次加载。这种方式减少了编译器的工作量,加快了构建速度,为大规模的开发提供了便利。此外,包的导入循环是不允许的:由于fmt导入io,io就不能导入fmt,也不能导入任何其他导入fmt的东西,即使是间接的。这也降低了编译器的工作量,确保了在单个单独编译的包的级别上对某个特定的构建进行拆分。这也使我们可以进行增量式的程序分析,即使在执行测试之前,我们也会执行这种分析来捕捉错误,如下所述。

导入fmt并不能使io.Writer这个名字对客户端可用。如果主包想使用io.Writer这个类型,那么它就必须为自己导入“io”。因此,一旦所有对fmt限定名称的引用被从源文件中删除——例如,如果import"fmt"调用被删除,import"fmt"语句就可以安全地从源文件中删除,而无需进一步分析。这个属性使得自动管理源代码中的导入成为可能。事实上,Go不允许未使用的导入,以避免将未使用的代码链接到程序中而造成的臃肿。

导入路径是带引号的字符串字面,这使其解释具有灵活性。斜线分隔的路径在导入时标识了import的包,但随后源代码会使用在包声明中声明的短标识符来引用该包。例如,import"net/

分享 转发
TOP
发新话题 回复该主题