Go 语言的设计反思

魏小言
云原生领域优质创作者
博客专家认证
2022-05-09 15:36:23

目录

起源

包(Package)

类型

并发

安全

完整性

一致性

工具辅助开发

库函数

结论

Go 是一种编程语言,2007 年底在谷歌创建,2009 年 11 月正式开源发布。从那时起,它开始作为一个公共项目运作,有成千上万的个人和数十家公司参与贡献。Go 已成为构建云基础设施的流行语言:Linux 容器管理器 Docker 和容器部署系统 Kubernetes 是用 Go 编写的核心云技术。今天,Go 是每个主要云提供商的关键基础设施的基础,并且是云原生计算基金会托管的大多数项目的实现语言。

早期用户被 Go 吸引的原因有很多。用于构建系统的垃圾收集、静态编译的语言,这并不常见。Go 对并发和并行的原生支持有助于利用当时成为主流的多核机器。自包含的二进制文件和简单的交叉编译简化了部署。而谷歌的名字无疑也是一个加成。

但是为什么用户能留下来?为什么Go 变得越来越流行,而许多其他语言项目却没有?我们相信语言本身只是答案的一小部分。完整的故事必须涉及整个 Go 环境:库、工具、约定和软件工程的整体方法,它们都支持使用该语言进行编程。因此,在语言设计中做出的最重要的决定是让 Go 更适合大型软件工程,并帮助我们吸引志同道合的开发人员。

本文研究了我们认为对 Go 的成功作出最大贡献的设计决策,探索它们如何不仅适用于语言,还适用于更广泛的环境。很难分离出某个特定决策的贡献,因此本文不应被视为科学分析,而应作为对过去十年经验和用户反馈的最佳理解的呈现。关注我 code 杂坛,了解更多......


起源
Go 源于在 Google 构建大型分布式系统的经验,在由数千名软件工程师共享的大型代码库中工作。我们希望为这种环境设计的语言和工具能够解决公司和整个行业面临的挑战。由于开发工作的规模和正在部署的生产系统的规模,挑战出现了。

发展规模。在开发方面,Google 在 2007 年有大约 4,000 名活跃用户使用单一、共享、多语言(C++、Java、Python)代码库。单一代码库可以很容易地修复,例如内存分配器的问题正在减慢主 Web 服务器的响应速度。但是在使用这些库时,很容易在不知不觉中破坏以前的客户端,因为很难找到包的所有依赖项。

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

为了解决这样的减速问题,我们开始着手开发一个新的、大规模并行和可缓存的构建系统,该系统最终成为开源的 Bazel 构建系统。但并行和缓存对于修复低效系统的作用有限。我们认为语言本身需要做更多的事情来提供帮助。

生产规模。在生产方面,Google运行着很多非常大的系统。例如,在 2005 年 3 月,Sawzall 日志分析系统的一个 1,500-CPU 集群处理了 2.8 PB 的数据。2006 年 8 月,Google 的 388 个 Bit-table 服务集群由 24,500 台平板服务器组成,其中一组 8,069 台服务器每秒处理总计 120 万个请求。

然而,Google和业内其他公司一样,都在努力编写高效的程序以充分利用多核系统。我们的许多系统都需要在一台机器上运行相同二进制文件的多个副本,因为现有的多线程支持既麻烦又低性能。大型、固定大小的线程堆栈、重量级堆栈开关以及用于创建新线程和管理它们之间交互的笨拙语法都使得使用多核系统变得更加困难。但很明显,服务器里的核数量只会持续增加。

在这里,我们也相信语言本身可以通过提供轻量级、易于使用的并发原语来提供帮助。我们还在这些额外的内核中看到了一个机会:垃圾收集器可以与专用内核上的主程序并行运行,从而降低其延迟成本。

Go 是我们对于应对这些挑战的语言可能是什么样子的问题的回答。毫无疑问,Go受欢迎的部分原因是整个科技行业现在每天都面临着这些挑战。云提供商使即使是最小的公司也可以实现非常大的生产部署。虽然大多数公司没有数千名活跃的员工在编写代码,但几乎所有公司现在都依赖于由数千名程序员开发的大量开源的基础设施。

本文的其余部分将探讨具体的设计决策如何实现这些扩展开发和生产的目标。我们从核心语言本身开始,向外拓展到周围环境。我们不会对语言进行完整的介绍。为此,请参阅 Go 语言规范或诸如《The Go Programming Language》等书籍。


包(Package)
Go 程序由一个或多个可导入包组成,每个包包含一个或多个文件。图 1 中的 Web 服务器说明了有关 Go 包系统设计的许多重要细节:

图 1. 一个 Go 的 Web 服务器

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

一个包使用显式 import 语句(第 3-6 行)导入另一个包,这与许多语言一样,但与 C++ 的文本 #include 机制相反。然而,与大多数语言不同的是,Go 的每次导入只读取一个文件。

例如,fmt 包的公开 API 引用来自 io 包的类型:fmt.Fprintf 的第一个参数是 io.Writer 类型的接口值。在大多数语言中,编译器处理 fmt 导入时还需要加载所有 io 以便理解 fmt 的定义,这反过来可能就需要加载额外的包以理解所有 io 的定义。单个导入语句可能最终需要处理数十或数百个包。

Go 避免了这项工作,类似于 Modula-2,为编译的 fmt 包的元数据安排包含了解其自身依赖项所需的所有内容,例如 io.Writer 的定义。因此,import "fmt" 的编译只读取一个完整描述 fmt 及其依赖关系的文件。此外,这种扁平化只进行一次,在编译 fmt 时进行,避免每次导入时的多次加载。

这种方法可以减少编译器的工作量并加快构建速度,从而有助于大规模开发。此外,包的循环导入是不允许的:因为 fmt 导入 io,io 不能导入 fmt,也不能导入其他导入了 fmt 的包,即使是间接导入。这也减少了编译器的工作量,确保特定构建可以在拆分成单个的、分别编译的包。这也就支持了增量程序分析,甚至在运行测试之前就可以运行它以捕获错误,如下所述。

导入 fmt 不会使 io.Writer 对客户端可用。如果主包想要使用 io.Writer 类型,它必须自己去 import "io"。因此,一旦从源文件中删除了对 fmt 限定名称的所有引用——例如,如果 fmt.Fprintf 调用被删除——import "fmt" 语句就可以安全地从源文件中删除而无需进一步分析。

这样的属性使得可以自动管理源代码中的导入。事实上,Go 不允许导入未被使用的包,以避免将未使用的代码链接到程序里而造成的不必要的膨胀。

导入路径是带引号的字符串文字,这样可以灵活地对其进行解释。斜杠分隔的路径在 import 中标识了导入的包,但是随后源代码使用包语句中声明的短标识符来引用包。例如,import "net/http" 声明了提供访问其内容的顶级名称 http。在标准库之外,包由以域名开头的类似 URL 的路径来标识,如 import "github.com/google/uuid"。稍后我们将对此类软件包进行更多说明。

最后一个细节,请注意名称 fmt.Fprintf 和 io.Writer 中的大写字母。Go 对 C++ 和 Java 的 public、private 和 protected 概念和关键字的模拟是一种命名约定。带有前导大写字母的名称,例如 Printf 和 Writer,被 “导出”(公开)。其他则不是。基于大小写、

编译器强制执行的导出规则适用于常量、函数和类型的包级标识符;方法名称;和结构字段名称。我们确定这条规则是为了避免在公开 API 中涉及的每个标识符旁边都需要写一个关键字(如 export)。随着时间的推移,我们开始重视查看标识符是在包外部可用还是纯粹在内部可用。关注我 code 杂坛,了解更多......


类型
Go 提供了一组常用的基本类型:布尔值、大小整数如 uint8 和 int32、无大小的 int 和 uint(32 位或 64 位,取决于机器大小),以及大小的浮点数和复数。

它以类似于 C 的方式提供指针、固定大小的数组和结构。它还提供内置的字符串类型、称为 map 的哈希表和称为 slice 的动态大小的数组。大多数 Go 程序都依赖这些类型,没有其他特殊的容器类型。

Go 不定义类,但允许将方法绑定到任何类型,包括结构、数组、slice、map 甚至是整数等基本类型。它没有类型的层次结构;我们认为继承往往会使程序在成长过程中更难适应。相反,Go 鼓励类型的组合。

如今,Go 是主流云提供商的关键基础架构的基石。

Go 通过其接口类型提供了面向对象的多态性。与 Java 接口或 C++ 抽象虚拟类一样,Go 接口包含方法名称和签名的列表。比如前面提到的 io.Writer 接口是在 io 包中定义的,如图 2 所示。

图 2. io 包的 Writer 接口

Write 接受一个字节 slice 并返回一个整数以及可能的错误。与 Java 和 C++ 不同,任何具有与接口相同名称和签名的方法的 Go 类型都被认为实现了该接口,而无需明确声明它这样做。例如,类型 os.File 有一个具有相同签名的 Write 方法,因此它实现了 io.Writer,而不需要像 Java 那样显式的 “implements” 注释。

避免接口和实现之间的显式关联允许 Go 程序员定义小的、灵活的、通常是 ad hoc 接口,而不是将它们用作复杂类型层次结构中的基础块。它鼓励在开发过程中捕获关系和操作,而不需要提前计划和定义它们。

这尤其有助于大型程序,在这些程序中,刚开始开发时,最终的结构更加难以看清。无需声明实现的方式鼓励使用精确的、一种或两种方法的接口,例如 Writer、Reader、Stringer(类似于 Java 的 toString 方法)等,这些接口遍布标准库。

首次学习 Go 的开发人员经常担心某个类型会意外地实现某个接口。虽然很容易建立假设,但实际上不太可能为两个不兼容的操作选择相同的名称和签名,而且我们从未在真正的 Go 程序中看到过这种情况。


————————————————
版权声明:本文为CSDN博主「魏小言」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/qq_34417408/article/details/124664528

...全文
325 回复 打赏 收藏 转发到动态 举报
AI 作业
写回复
用AI写文章
回复
切换为时间正序
请发表友善的回复…
发表回复

16

社区成员

发帖
与我相关
我的任务
社区描述
code杂坛:提供初级同学进阶的深度与广度!关注一线 “ 互联网时讯、各技术栈、开源产品、面试技巧......“ 等最新动态
云原生架构设计模式 个人社区
社区管理员
  • 魏小言
  • flybirding10011
加入社区
  • 近7日
  • 近30日
  • 至今
社区公告

hi,欢迎加入 “code杂坛” 社区!

code杂坛:提供初级同学进阶的深度与广度!关注一线 “ 互联网时讯、各技术栈、开源产品、面试技巧......“ 等最新动态

在这里你可以:

  • 学习最新大厂技术知识
  • 掌握技能进阶各种技巧
  • 交到志同道合的朋友
  • 获取最新图书资讯
  • 参与活动免费赠书
  • 与我们的作者、译者互动!

 

加入我们成为 code杂坛 的同学

【欢迎联系】

微信公众号:code杂坛

试试用AI创作助手写篇文章吧