Go 语言的设计哲学
从简单、显式、组合、并发和面向工程五个方面理解 Go 的语言、标准库与工具链设计。
Go 语言的设计哲学
简单
简单的设计哲学是Go生产力的源泉。
显式
在 Go 语言中,不同类型的值通常不能直接混合计算。开发者需要显式转换类型,让类型边界和可能的数据变化在代码中清楚可见。
组合
Go 语言为支撑组合的设计提供了类型嵌入(Type Embedding)。通过类型嵌入,我们可以将已经实现的功能嵌入到新类型中,以快速满足新类型的功能需求,这种方式有些类似经典面向对象语言中的“继承”机制,但在原理上却与面向对象中的继承完全不同,这是一种 Go 设计者们精心设计的“语法糖”。
嵌入类型与外层类型之间不是经典面向对象语言中的父子类关系,也不存在继承体系中的向上、向下转型。嵌入字段的方法可以被提升到外层类型的方法集合中,但选择器解析仍受 Go 类型系统、嵌入层级和名称冲突规则约束。这种通过嵌入复用能力的方式,可以理解为一种垂直组合。
下面这个 Go 标准库中的一段使用类型嵌入的组合方式的代码段:
// $GOROOT/src/sync/pool.go
type poolLocal struct {
private interface{}
shared []interface{}
Mutex
pad [128]byte
}
在代码段中,我们在 poolLocal 这个结构体类型中嵌入了类型 Mutex,这就使得 poolLocal 这个类型具有了互斥同步的能力,我们可以通过 poolLocal 类型的变量,直接调用 Mutex 类型的方法 Lock 或 Unlock。
另外,我们在标准库中还会经常看到类似如下定义接口类型的代码段:
// $GOROOT/src/io/io.go
type ReadWriter interface {
Reader
Writer
}
这里,标准库通过嵌入接口类型的方式来实现接口行为的聚合,组成大接口,这种方式在标准库中尤为常用,并且已经成为了 Go 语言的一种惯用法。
垂直组合本质上是一种“能力继承”,采用嵌入方式定义的新类型继承了嵌入类型的能力。Go 还有一种常见的组合方式,叫水平组合。和垂直组合的能力继承不同,水平组合是一种能力委托(Delegate),我们通常使用接口类型来实现水平组合。
Go 语言中的接口是一个创新设计,它只是方法集合,并且它与实现者之间的关系无需通过显式关键字修饰,它让程序内部各部分之间的耦合降至最低,同时它也是连接程序各个部分之间“纽带”。
水平组合的模式有很多,比如一种常见方法就是,通过接受接口类型参数的普通函数进行组合,如以下代码段所示:
// Go 1.16 起使用 io.ReadAll;早期版本位于 io/ioutil
func ReadAll(r Reader) ([]byte, error)
// $GOROOT/src/io/io.go
func Copy(dst Writer, src Reader)(written int64, err error)
也就是说,函数 io.ReadAll 通过 io.Reader 接口,与各种具体数据源低耦合地组合,从任意实现该接口的数据源读取内容。类似的组合模式还有装饰器、中间件等。
我们还可以将 Go 语言内置的并发能力进行灵活组合以实现,比如,通过 goroutine+channel 的组合,可以实现类似 Unix Pipe 的能力。
总之,组合原则的应用实质上是塑造了 Go 程序的骨架结构。类型嵌入为类型提供了垂直扩展能力,而接口是水平组合的关键,它好比程序肌体上的“关节”,给予连接“关节”的两个部分各自“自由活动”的能力,而整体上又实现了某种功能。并且,组合也让遵循“简单”原则的 Go 语言,在表现力上丝毫不逊色于其他复杂的主流编程语言。
并发
“并发”这个设计哲学的出现有它的背景,你也知道 CPU 都是靠提高主频来改进性能的,但是现在这个做法已经遇到了瓶颈。主频提高导致 CPU 的功耗和发热量剧增,反过来制约了 CPU 性能的进一步提高。2007 年开始,处理器厂商的竞争焦点从主频转向了多核。
在这种大背景下,Go 的设计者在决定去创建一门新语言的时候,果断将面向多核、原生支持并发作为了新语言的设计原则之一。并且,Go 放弃了传统的基于操作系统线程的并发模型,而采用了用户层轻量级线程,Go 将之称为 goroutine。
goroutine 的初始栈通常很小,并能按需增长和收缩;具体初始大小属于运行时实现细节,不应作为稳定 API 依赖。goroutine 由 Go 运行时调度,创建和切换成本通常低于直接为每个任务创建操作系统线程,因此一个程序可以按实际资源和负载创建大量并发任务。
在提供了开销较低的 goroutine 的同时,Go 还在语言层面内置了辅助并发设计的原语:channel 和 select。开发者可以通过语言内置的 channel 传递消息或实现同步,并通过 select 实现多路 channel 的并发控制。相较于传统复杂的线程并发模型,Go 对并发的原生支持将大大降低开发人员在开发并发程序时的心智负担。
此外,并发的设计哲学不仅仅让 Go 在语法层面提供了并发原语支持,其对 Go 应用程序设计的影响更为重要。并发是一种程序结构设计的方法,它使得并行成为可能。
采用并发结构的程序在单核处理器上也可以运行。多核为并行执行提供了条件,但性能是否提升仍取决于任务能否并行、同步开销、I/O、调度和算法设计;并发本身不保证更快。
而且,并发与组合的哲学是一脉相承的,并发是一个更大的组合的概念,它在程序设计的全局层面对程序进行拆解组合,再映射到程序执行层面上:goroutines 各自执行特定的工作,通过 channel+select 将 goroutines 组合连接起来。并发的存在鼓励程序员在程序设计时进行独立计算的分解,而对并发的原生支持让 Go 语言也更适应现代计算环境。
面向工程
Go 语言设计的初衷,就是面向解决真实世界中 Google 内部大规模软件开发存在的各种问题,为这些问题提供答案,这些问题包括:程序构建慢、依赖管理失控、代码难于理解、跨语言构建难等
很多编程语言设计者和他们的粉丝们认为这些问题并不是一门编程语言应该去解决的,但 Go 语言的设计者并不这么看,他们在 Go 语言最初设计阶段就将解决工程问题作为 Go 的设计原则之一去考虑 Go 语法、工具链与标准库的设计,这也是 Go 与其他偏学院派、偏研究型的编程语言在设计思路上的一个重大差异。
语法是编程语言的用户接口,它直接影响开发人员对于这门语言的使用体验。在面向工程设计哲学的驱使下,Go 在语法设计细节上做了精心的打磨。比如:
-
重新设计编译单元和目标文件格式,实现 Go 源码快速构建,让大工程的构建时间缩短到类似动态语言的交互式解释的编译速度;
-
如果源文件导入它不使用的包,则程序将无法编译。这可以充分保证任何 Go 程序的依赖树是精确的。这也可以保证在构建程序时不会编译额外的代码,从而最大限度地缩短编译时间;
-
去除包的循环依赖,循环依赖会在大规模的代码中引发问题,因为它们要求编译器同时处理更大的源文件集,这会减慢增量构建;(比如java中的循环依赖)
-
包路径是唯一的,而包名不必唯一的。导入路径必须唯一标识要导入的包,而名称只是包的使用者如何引用其内容的约定。“包名称不必是唯一的”这个约定,大大降低了开发人员给包起唯一名字的心智负担;
-
故意不支持默认函数参数。因为在规模工程中,很多开发者利用默认函数参数机制,向函数添加过多的参数以弥补函数 API 的设计缺陷,这会导致函数拥有太多的参数,降低清晰度和可读性;
-
增加类型别名(type alias),支持大规模代码库的重构。
在标准库方面,Go 被称为“自带电池”的编程语言。如果说一门编程语言是“自带电池”,则说明这门语言标准库功能丰富,多数功能不需要依赖外部的第三方包或库,Go 语言恰恰就是这类编程语言。
Go 标准库提供了 net/http、crypto、encoding 等常用包,开发者可以仅依靠标准库搭建完整的 HTTP 服务基础。真正达到生产要求仍需要结合业务补充认证、授权、可观测性、配置、安全策略和部署保障,但标准库能够减少不必要的第三方依赖。
Go 提供了覆盖编译构建、代码格式化、依赖管理、静态检查、测试、文档、性能剖析、语言服务器和运行时跟踪等场景的统一工具链。
这里值得重点介绍的是 gofmt ,它统一了 Go 语言的代码风格,在其他语言开发者还在为代码风格争论不休的时候,Go 开发者可以更加专注于领域业务中。同时,相同的代码风格让以往困扰开发者的代码阅读、理解和评审工作变得容易了很多,至少 Go 开发者再也不会有那种因代码风格的不同而产生的陌生感。Go 的这种统一代码风格思路也在开始影响着后续新编程语言的设计,并且一些现有的主流编程语言也在借鉴 Go 的一些设计。
在提供丰富的工具链的同时,Go 在标准库中提供了官方的词法分析器、语法解析器和类型检查器相关包,开发者可以基于这些包快速构建并扩展 Go 工具链
总结
Go 语言的设计哲学:简单、显式、组合、并发和面向工程。
-
简单是指 Go 语言特性始终保持在少且足够的水平,不走语言特性融合的道路,但又不乏生产力。简单是 Go 生产力的源泉,也是 Go 对开发者的最大吸引力;
-
显式是指关键类型转换、错误处理和依赖关系尽量直接体现在代码中,以降低隐式行为带来的理解成本;
-
组合是构建 Go 程序骨架的主要方式,它可以大幅降低程序元素间的耦合,提高程序的可扩展性和灵活性;
-
并发是 Go 面向多核和高并发工作负载的重要设计选择,让开发者更容易表达可独立执行并相互协作的任务;是否获得性能提升仍需通过测量验证;
-
面向工程是 Go 语言在语言设计上的一个重大创新,它将语言要解决的问题域扩展到那些原本并不是由编程语言去解决的领域,从而覆盖了更多开发者在开发过程遇到的“痛点”,为开发者提供了更好的使用体验。
这些设计哲学直接影响了 Go 语言自身的设计。理解这些设计哲学,也能帮助我们理解 Go 语言语法、标准库以及工具链的演化决策过程。
有哪里没看懂?可以只问这篇。
Jarvis 会限定在《Go 语言的设计哲学》及其公开关联内容中检索,并把引用定位回原文章节。