Go语言精进之路:从新手到高手的编程思想、方法和技巧(1)
上QQ阅读APP看书,第一时间看更新

3.4 面向工程,“自带电池”

软件工程指引着Go语言的设计。

——Rob Pike(2012)

要想理解这条设计哲学,我们依然需要回到三位Go语言之父在设计Go语言时的初衷:面向真实世界中Google内部大规模软件开发存在的各种问题,为这些问题提供答案。主要的问题包括:

  • 程序构建慢;
  • 失控的依赖管理;
  • 开发人员使用编程语言的不同子集(比如C++支持多范式,这样有些人用OO,有些人用泛型);
  • 代码可理解性差(代码可读性差、文档差等);
  • 功能重复实现;
  • 升级更新消耗大;
  • 实现自动化工具难度高;
  • 版本问题;
  • 跨语言构建问题。

很多编程语言的设计者或拥趸认为这些问题并不是编程语言应该解决的,但Go语言的设计者并不这么看,他们以更高、更广阔的视角审视软件开发领域尤其是大规模软件开发过程中遇到的各种问题,并在Go语言最初设计阶段就将解决工程问题作为Go的设计原则之一去考虑Go语法、工具链与标准库的设计,这也是Go与那些偏学院派、偏研究性编程语言在设计思路上的一个重大差异。

Go语言取得阶段性成功后,这种思路开始影响后续新编程语言的设计,并且一些现有的主流编程语言也在借鉴Go的一些设计,比如越来越多的语言认可统一代码风格的优越之处,并开始提供官方统一的fmt工具(如Rust的rustfmt),又如Go创新提出的最小版本选择(Minimal Version Selection,MVS)被其他语言的包依赖工具所支持(比如Rust的cargo支持MVS)。

Go设计者将所有工程问题浓缩为一个词:scale(笔者总觉得将scale这个词翻译为任何中文词都无法传神地表达其含义,暂译为“规模”吧)。从Go1开始,Go的设计目标就是帮助开发者更容易、更高效地管理两类规模。

  • 生产规模:用Go构建的软件系统的并发规模,比如这类系统并发关注点的数量、处理数据的量级、同时并发与之交互的服务的数量等。
  • 开发规模:包括开发团队的代码库的大小,参与开发、相互协作的工程师的人数等。

Go设计者期望Go可以游刃有余地应对生产规模和开发规模变大带来的各种复杂问题。Go语言的演进方向是优化甚至消除Go语言自身面对规模化问题时应对不好的地方,比如:Go 1.9引入类型别名(type alias)以应对大型代码仓库代码重构,Go 1.11引入go module机制以解决不完善的包依赖问题等。这种设计哲学的落地让Go语言具有广泛的规模适应性:既可以被仅有5人的初创团队用于开发终端工具,也能够满足像Google这样的巨型公司大规模团队开发大规模网络服务程序的需要。

那么Go是如何解决工程领域规模化所带来的问题的呢?我们从语言、标准库和工具链三个方面来看一下。

(1)语言

语法是编程语言的用户接口,它直接影响开发人员对于一门语言的使用体验。Go语言是一门简单的语言,简单意味着可读性好,容易理解,容易上手,容易修复错误,节省开发者时间,提升开发者间的沟通效率。但作为面向工程的编程语言,光有简单的设计哲学还不够,每个语言设计细节还都要经过“工程规模化”的考验和打磨,需要在细节上进行充分的思考和讨论。

比如Rob Pike就曾谈到,Go当初之所以没有使用Python那样的代码缩进而是选择了与C语言相同的大括号来表示程序结构,是因为他们经过调查发现,虽然Python的缩进结构在构建小规模程序时的确很方便,但是当代码库变得更大的时候,缩进式的结构非常容易出错。从工程的安全性和可靠性角度考虑,Go团队最终选择了大括号代码块结构。

类似的面向工程的语言设计细节考量还有以下这些。

  • 重新设计编译单元和目标文件格式,实现Go源码快速构建,将大工程的构建时间缩短到接近于动态语言的交互式解释的编译时间。
  • 如果源文件导入了它不使用的包,则程序将无法编译。这既可以充分保证Go程序的依赖树是精确的,也可以保证在构建程序时不会编译额外的代码,从而最大限度地缩短编译时间。
  • 去除包的循环依赖。循环依赖会在大规模的代码中引发问题,因为它们要求编译器同时处理更大的源文件集,这会减慢增量构建速度。
  • 在处理依赖关系时,有时会通过允许一部分重复代码来避免引入较多依赖关系。比如:net包具有其自己的整数到十进制转换实现,以避免依赖于较大且依赖性较强的格式化io包。
  • 包路径是唯一的,而包名不必是唯一的。导入路径必须唯一标识要导入的包,而名称只是包的使用者对如何引用其内容的约定。包名不必是唯一的约定大大降低了开发人员给包起唯一名字的心智负担。
  • 故意不支持默认函数参数。因为在规模工程中,很多开发者利用默认函数参数机制向函数添加过多的参数以弥补函数API的设计缺陷,这会导致函数拥有太多的参数,降低清晰度和可读性。
  • 首字母大小写定义标识符可见性,这是Go的一个创新。它让开发人员通过名称即可知晓其可见性,而无须回到标识符定义的位置查找并确定其可见性,这提升了开发人员阅读代码的效率。
  • 在语义层面,相对于C,Go做了很多改动,提升了语言的健壮性,比如去除指针算术,去除隐式类型转换等。
  • 内置垃圾收集。这对于大型工程项目来说,大大降低了程序员在内存管理方面的负担,程序员使用GC感受到的好处超过了付出的成本,并且这些成本主要由语言实现者来承担。
  • 内置并发支持,为网络软件带来了简单性,而简单又带来了健壮,这是大型工程软件开发所需要的。
  • 增加类型别名,支持大规模代码库的重构。

(2)标准库

Go被称为“自带电池”(battery-included)的编程语言。“自带电池”原指购买了电子设备后,在包装盒中包含了电池,电子设备可以开箱即用,无须再单独购买电池。如果说一门编程语言“自带电池”,则说明这门语言标准库功能丰富,多数功能无须依赖第三方包或库,Go语言恰是这类编程语言。由于诞生年代较晚,且目标较为明确,Go在标准库中提供了各类高质量且性能优良的功能包,其中的net/http、crypto/xx、encoding/xx等包充分迎合了云原生时代关于API/RPC Web服务的构建需求。Go开发者可以直接基于这些包实现满足生产要求的API服务,从而减轻对第三方包或库的依赖,降低工程代码依赖管理的复杂性,也降低开发人员学习第三方库的心智负担。

仅使用标准库来构建系统,这对于开发人员是很有吸引力的。在很多关于选用何种Go Web开发框架的调查中,选择标准库的依然占大多数,这也是Go社区显著区别于其他编程语言社区的一点。Go团队还在golang.org/x路径下提供了暂未放入标准库的扩展库/补充库供广大Gopher使用,包括text、net、crypto等。这些库的质量也是非常高的,标准库中部分包也将golang.org/x下的text、net和crypto包作为依赖包放在标准库的vendor目录中。

Go语言目前在GUI、机器学习(Machine Learning)等开发领域占有的份额较低,这很可能与Go标准库没有内置这类包有关。在2017年的Go语言用户调查[3]中,Gopher最希望标准库增加的功能中,GUI、机器学习包就排名靠前,见图3-7。(2017年以后的Go用户调查中,该问题没有被列入调查项当中,因此这里使用了2017年的数据。)

040-1

图3-7 2017年Go语言用户调查结果节选

这或多或少反向证明了“内置电池”对于解决工程领域问题的重要性。

(3)工具链

开发人员在做工程的过程中需要使用工具。而Go语言提供了十分全面、贴心的编程语言官方工具链,涵盖了编译、编辑、依赖获取、调试、测试、文档、性能剖析等的方方面面。

  • 构建和运行:go build/go run
  • 依赖包查看与获取:go list/go get/go mod xx
  • 编辑辅助格式化:go fmt/gofmt
  • 文档查看:go doc/godoc
  • 单元测试/基准测试/测试覆盖率:go test
  • 代码静态分析:go vet
  • 性能剖析与跟踪结果查看:go tool pprof/go tool trace
  • 升级到新Go版本API的辅助工具:go tool fix
  • 报告Go语言bug:go bug

值得重点提及的是gofmt统一了Go语言的编码风格,在其他语言开发者还在为代码风格争论不休的时候,Go开发者可以更加专注于领域业务。同时,相同的代码风格让以往困扰开发者的代码阅读、理解和评审工作变得容易了很多,至少Go开发者再也不会有那种因代码风格的不同而产生的陌生感。

在提供丰富的工具链的同时,Go语言的语法、包依赖系统以及命名惯例的设计也让针对Go的工具更容易编写,并且Go在标准库中提供了官方的词法分析器、语法解析器和类型检查器相关包,开发者可以基于这些包快速构建并扩展Go工具链。

可以说Go构建了一个开放的工具链生态系统,它鼓励社区和开发人员为Go添加更多、更实用的工具,而更多、更实用的工具反过来又帮助Go更好地解决工程上的“规模化”问题,这是一个良性的生态循环。

小结

简单是Go语言贯穿语言设计和应用的主旨设计哲学。德国建筑大师路德维希·密斯·凡德罗将“少即是多”这一哲学理念应用到建筑设计当中后取得了非凡的成功,而Go语言则是这一哲学在编程语言领域为数不多的践行者。“少”绝不是目的,“多”才是其内涵。Go在语言层面的简单让Go收获了不逊于C++/Java等的表现力的同时,还获得了更好的可读性、更高的开发效率等在软件工程领域更为重要的元素。

“高内聚、低耦合”是软件开发领域亘古不变的管理复杂性的准则。Go在语言设计层面也将这一准则发挥到极致。Go崇尚通过组合的方式将正交的语法元素组织在一起来形成应用程序骨架,接口就是在这一哲学下诞生的语言精华。

不同于C、C++、Java等诞生于20世纪后段的面向单机的编程语言,Go语言是面向未来的。Go设计者对硬件发展趋势做出了敏锐且准确的判断——多核时代是未来主流趋势,于是将并发作为语言的“一等公民”,提供了内置于语言中的简单并发原语——go(goroutine)、channel和select,大幅降低了开发人员在云计算多核时代编写大规模并发网络服务程序时的心智负担。

Go生来就肩负着解决面向软件工程领域问题的使命,我们看到的开箱即用的标准库、语言自带原生工具链以及开放的工具链生态的建立都是这一使命落地的结果,Go在面向工程领域的探索也引领着编程语言未来发展的潮流。


[1]Go语言2016年调查结果:https://blog.golang.org/survey2016-results

[2]https://talks.golang.org/2012/waza.slide

[3]https://blog.golang.org/survey2017-results