



此时此刻,想必你已经跃跃欲试,想要立即开启Go编程的学习之旅。但在正式学习Go语法之前,我还是要给你 泼冷水 ,因为这将决定你后续的学习结果是“从入门到继续”还是“从入门到放弃”。
很多编程语言的初学者在学习过程中可能会遇到这样的问题:起初满怀热情地学习一种新编程语言,但随着学习的深入,开始发现一些令人感到“别扭”的地方,例如,想要的语言特性缺失、语法风格冷僻且与主流语言差异较大、语言的不同版本间无法兼容、语言的语法特性过多、语言的工具链支持较差等。
其实,以上问题本质上都与编程语言设计者的设计哲学有关。所谓编程语言的设计哲学,就是指指导编程语言演化进程的高级原则和依据。
设计哲学之于编程语言,就好比价值观之于人的行为。
如果你不认同一个人的价值观,那你其实很难与之持续交往。类似地,如果你不认同一种编程语言的设计哲学,那么大概率你在后续的语言学习中会遇到上面提到的这些问题,甚至可能会失去继续学习的动力。
因此,在深入学习Go语法和编程之前,了解Go的设计哲学是十分必要的。学完这一节的内容之后,你就能更深刻地认识到自己学习Go的原因。
我将Go的设计哲学总结为5点——简单、显式、组合、并发和面向工程。接下来我们从Go的第1个设计哲学“简单”开始探讨。
Go开发者戴维·切尼(Dave Cheney)说:“大多数编程语言在创建时都致力于成为一种简单的编程语言,但最终往往满足于成为一种强大的编程语言。”而Go则是一个例外。从设计之初, Go的创造者们就选择了“做减法”,专注于打造一种简单的编程语言,而非融合多种语言特性的综合体 。
选择“简单”意味着Go不会像C++、Java那样吸收其他编程语言的新特性。所以,在Go中你看不到传统的面向对象元素,如类、构造函数与继承;没有结构化的异常处理机制;也缺乏属于函数编程范式的语法结构。
其实,Go也没它看起来那么简单,其复杂性被精心“隐藏”了,使得语法层面呈现出以下状态。
❏ 仅有25个关键字,是主流编程语言中最少的。
❏ 内置垃圾收集机制,减轻了开发者的内存管理负担。
❏ 可见性通过标识符首字母大小写决定,无需额外的关键字修饰。
❏ 变量默认初始化为其类型的零值,避免了未初始化变量的问题。
❏ 内置数组边界检查,减少了越界访问安全隐患。
❏ 内置并发支持,简化了并发程序的设计。
❏ 内置接口类型,为组合的设计哲学奠定了基础。
❏ 提供了完善的工具链,开箱即用。
当然, 任何设计都是权衡与取舍的结果 。Go的设计者们 站在巨人的肩膀上,移除或优化了那些已经被证明对开发者不友好或难以掌握的语法元素和语言机制,并引入了一些创新设计 。
Go这种“逆潮流”的“简单哲学”起初可能不易被开发者理解,但在真正使用之后,开发者才能体会到它所带来的好处: 简单不仅意味着可以使用更少的代码实现相同的功能,还意味着代码更易于阅读、维护和调试 。
总之,在现代软件工程中,Go的简单设计哲学提升了生产效率,我们可以据此认为 它是Go生产力的核心所在 。
为了更好地理解“显式”的概念,我们先来看一段C程序,对比一下“隐式”代码的行为特征。在C语言中,下面这段代码可以正常编译并输出正确结果。
// ch1/explicit.c
#include <stdio.h>
int main() {
short int a = 5;
int b = 8;
long c = 0;
c = a + b;
printf(“%ld\n”, c);
}
在上面这段代码中,尽管变量a、b和c的类型不同,但C语言编译器会在编译时自动将短整型变量a和整型变量b转换为long类型后相加,并将结果存储在long类型变量c中。如果由Go实现类似计算,结果会怎么样呢?我们先把上面的C程序转化成等价的Go代码。
// ch1/explicit.go
package main
import “fmt”
func main() {
var a int16 = 5
var b int = 8
var c int64
c = a + b
fmt.Printf(“%d\n”, c)
}
如果编译这段Go代码,将得到类似这样的编译器错误:“invalid operation: a + b (mismatched types int16 and int)”。这表明与C语言不同,Go不允许不同类型的整型变量直接混合计算,也不会进行隐式的类型转换。
为了让代码通过编译,对变量a和b进行 显式转型 ,就像下面这段代码。
c = int64(a) + int64(b) fmt.Printf(“%d\n”, c)
这段代码正是Go 显式设计哲学 的一个体现。
在Go中,不同类型变量不能直接混合计算,因为 Go希望开发者明确知道他们在做什么 ,这与C语言中的“信任开发者”原则完全不同,因此,需要以显式转型统一参与计算的变量类型。
此外,Go设计者所崇尚的显式设计哲学还体现在其错误处理方式上:Go采用了 基于值比较的显示错误处理方案 ,函数或方法中的错误都会通过return语句显式返回,并且通常调用者不应忽略这些返回的错误。
组合设计哲学与程序之间的耦合有关。不同于C++、Java等主流面向对象编程语言,Go并不包含经典的面向对象语法元素、类型体系和继承机制。相反,Go推崇 基于组合的设计理念 。
在解释组合之前,我们先了解一下Go在语法元素设计上如何为“组合”设计哲学的应用奠定基础。
❏ Go没有类型层次体系,各类型之间相互独立,不存在子类型的概念。
❏ 每个类型都可以有自己的方法集合,类型定义与方法实现是正交且独立的。
❏ 当实现某个接口时,无须像Java那样采用特定关键字修饰。
❏ 包之间相对独立,不存在子包的概念。
通过这些设计,无论是包、接口还是具体的类型定义,Go其实为我们呈现了一幅图景:一座座功能完备但相互独立的“孤岛”。而我们的任务则是在这些孤岛之间以最适当的方式建立关联,形成一个整体。 组合方式是Go最主要的方式。
为了支持组合设计理念,Go引入了 类型嵌入 (type embedding)。通过类型嵌入,我们可以将已经实现的功能嵌入新类型中,快速满足新类型的功能需求。这种方式虽然类似经典面向对象编程语言中的“继承”,但在原理上与其完全不同,它是一种精心设计的“语法糖”。
被嵌入的类型和新类型之间没有任何直接关系,它们甚至不知道对方的存在,更没有传统面向对象编程语言中的父类、子类关系及转型转换。当通过新类型实例调用方法时,方法匹配主要依赖于方法名称,而不是类型。这种组合方式被称为 垂直组合 ,即通过类型嵌入让新类型“复用”其他类型已有的功能,实现功能的垂直扩展。
例如,在Go标准库,使用类型嵌入的组合方式的代码段如下。
// $GOROOT/src/sync/pool.go
type poolLocal struct {
private interface{}
shared []interface{}
Mutex
pad [128]byte
}
在以上代码段中,我们在poolLocal结构体中嵌入了Mutex类型,使得poolLocal具有了互斥同步的能力,并可以直接调用Mutex类型的方法,如Lock或Unlock。
此外,标准库中常见的是通过嵌入接口类型来聚合行为,形成更大的接口。代码示例如下。
// $GOROOT/src/io/io.go
type ReadWriter interface {
Reader
Writer
}
垂直组合本质上是一种“能力继承”,而Go还有一种常见的组合方式,叫 水平组合 。与垂直组合的能力继承不同,水平组合是一种能力委托(delegate),通常通过接口类型来实现。
Go中的接口设计是一项创新,它仅仅是一个方法集合,并且接口与实现者之间的关系无需通过显式关键字来修饰。这种设计使得程序内各部分之间的耦合度降至最低,同时接口也充当了连接程序不同部分的“纽带”。
水平组合模式在Go中有多种表现形式,其中一种常见的方式是通过接受接口类型参数的普通函数进行组合。例如,以下代码段展示了这一模式的应用。
// $GOROOT/src/io/ioutil/ioutil.go func ReadAll(r io.Reader)([]byte, error) // $GOROOT/src/io/io.go func Copy(dst Writer, src Reader)(written int64, err error)
在上述代码段中,ReadAll函数通过io.Reader接口将任意实现了io.Reader的数据源与ReadAll所在包以低耦合的方式水平地组合在一起,从而能够从任何符合io.Reader接口的对象读取所有数据。
此外,我们还可以将Go内置的并发能力进行灵活组合,例如,利用goroutine+channel的组合,可以实现类似UNIX 管道的功能。
总之, 组合原则是Go程序结构的构建核心 。类型嵌入为类型提供了垂直扩展能力,而接口是水平组合的关键,它好比程序肌体上的“关节”,给予各个组件独立运作的能力,而整体上又实现了某种功能。通过这种方式,即使遵循“简单”设计哲学的Go,在表现力上也不逊色于其他复杂的主流编程语言。
在前面的内容中,我们已经探讨了Go的3个设计哲学,接下来我们将探讨第4个设计哲学—— 并发 。
并发这个设计哲学的提出有其背景。随着CPU主频提升以提升性能的传统做法遇到瓶颈,因为更高的主频导致了功耗和发热量的剧增,反而限制了CPU性能的进一步提升。自2007年开始,处理器厂商的竞争焦点 从单一高主频转向多核架构 。
在这种大背景下,Go的设计者决定将面向多核处理和 原生支持并发 作为新编程语言的设计原则之一。不同于传统的基于操作系统线程的并发模型,Go选择了 用户层轻量级线程 ,即 goroutine 。
每个goroutine占用的资源非常少,Go运行时默认为每个goroutine分配的栈空间仅为2KB
。goroutine之间的调度切换无需陷入(trap)操作系统内核层,因此开销非常低。这使得一个Go程序能够轻松创建成千上万个并发的goroutine。事实上,所有的Go代码都在goroutine中执行,包括Go运行时自身的代码。
除了提供开销较低的goroutine以外,Go还在语言层面内置了辅助并发设计的原语——channel和select。开发者可以利用channel传递消息或实现同步,并通过select实现多路channel的并发控制。相较于传统复杂的线程并发模型,Go对并发的原生支持大大减轻了开发者编写并发程序的负担。
并发的设计哲学不仅体现在语法层面上对并发原语的支持,更重要的是对程序设计的影响。 并发作为一种程序结构设计的方法 ,使并行计算成为可能。
采用并发方案设计的程序即使在单核处理器上也能正常运行,尽管其性能可能不如非并发方案,但随着处理器核数的增加,并发方案可以自然地提升处理效率。
此外,并发与组合的设计哲学相辅相成。并发是一种更大范围内的组合概念,它在全局层面对程序进行拆解和重组,再映射到程序执行层面:各个goroutine负责执行特定任务,而通过channel+select机制将这些goroutine连接起来。
并发的存在促使开发者在设计程序时分解独立的计算任务,而Go对并发的原生支持使其更加适应现代计算环境的需求。
最后,我们探讨Go的第5个设计哲学—— 面向工程 。
期刊 Communications of the ACM 在2022年5月第65卷第5期刊登了一篇由Go开发团队成员共同撰写的综述论文《Go编程语言与环境》,文章深入分析了那些对Go的成功最具决定性的设计哲学与决策。文章认为,Go之所以自诞生以来越来越流行,并非仅仅因为语言本身的优势,而是整个Go生态系统——包括 库、工具、惯用法和针对软件工程的整体做法 ,它们为使用Go进行编程提供了全面的支持。
Go的创造者认为,语言并不是项目的全部。他们最初的目标不是创造一门新的编程语言,而是 探索一种更好的编写软件的方式, 即更优的软件工程实践,尤其是针对大规模软件工程。罗伯·派克在2013年的SPLASH会议上发表了题为《Google的Go:面向软件工程的语言设计》的演讲,正式向世界宣告了这一点。
Go的设计初衷是 为了解决Google内部大规模软件开发中存在的各种实际问题 。这些问题包括程序构建速度慢、依赖管理失控、代码难以理解、跨语言构建难等。
很多编程语言的创造者及其追随者认为这些问题不应由编程语言本身来解决,但Go的创造者并不这么认为。他们在Go的设计初期就 将解决工程问题作为一项基本原则,贯穿于 语法、工具链与标准库的设计中。这也是Go与其他偏学院派、偏研究型的编程语言在设计思路上的一个重大差异。
语法是编程语言的用户接口,直接影响开发者对这门语言的使用体验。在面向工程设计哲学的指导下,Go在语法设计细节上进行了精心打磨。具体表现在以下几个方面。
❏ 重新设计了编译单元和目标文件格式,实现了Go源码的快速构建,使大型工程的构建时间缩短,具备接近动态编程语言交互式解释的速度。
❏ 如果源文件导入了未使用的包,程序将无法通过编译。这确保了任何Go程序的依赖树都是精确的,并且在构建程序时不会编译多余的代码,从而最大限度地缩短编译时间。
❏ 避免了包之间的循环依赖问题,因为这些依赖会在大规模的代码中引发复杂性,要求编译器处理更大的源文件集,降低增量构建速度。
❏ 虽然包名不必唯一,但是包路径必须唯一标识要导入的包。这个约定降低了开发者给每个包起唯一名称的心智负担。
❏ 故意不支持默认函数参数,以防止开发者通过添加过多参数来弥补API设计缺陷,保持函数签名的清晰度和可读性。
❏ 引入类型别名(type alias),有助于大规模代码库的重构工作。
在标准库方面,Go被称为“自带电池”的编程语言,意指其标准库功能丰富,大多数需求不需要依赖外部第三方包或库。
由于诞生较晚且目标明确,Go标准库提供了高质量且性能优良的功能包,如net/http、crypto、encoding等,充分满足了云原生时代关于API/RPC Web服务构建的需求。Go开发者可以直接利用这些标准库实现生产级别的API服务,减少对外部依赖的需求,降低工程代码依赖管理的复杂性,节省学习第三方库的时间和精力。
此外,Go提供了一套令其他主流编程语言开发者羡慕的工具链,涵盖从编译构建、代码格式化、包依赖管理到静态代码检查、测试、文档生成与查看、性能剖析、语言服务器以及运行时程序跟踪等方方面面。
这里值得重点介绍的是gofmt,它统一了Go的代码风格,使得Go开发者可以更加专注于业务逻辑,而不必纠结于代码风格的差异。同时,相同的代码风格大幅简化了代码阅读、理解和评审工作,消除了因代码风格不同而带来的陌生感。Go的这种统一代码风格的做法也影响了后续新编程语言的设计,一些现有的主流编程语言也在借鉴Go的相关设计理念。
同时,Go在标准库中提供了官方的词法分析器、语法解析器和类型检查器相关包,允许开发者基于这些包快速构建并扩展Go工具链。