Go 设计思想
date
Oct 29, 2023
slug
The design philosophy of Go
status
Published
tags
Go
读书笔记
summary
简单是一种美德。
type
Post
学习一门编程语言不仅要掌握它的语法,还要进一步学习它的设计思想,这能让我们写出更高质量的代码,下面我们一起来学习 Go 的设计思想。
简单是一种美德
简单是一种伟大的美德 —— Edsger Dijkstra,图灵奖得主。
关键字
首先我们来看关键字方面,Go 仅仅有 25 个关键字:
声明 | 复合类型 | 控制流 | 控制流 | 函数修饰语 |
const | chan | break | goto | defer |
var | interface | case | if | go |
func | map | continue | range | ㅤ |
type | struct | default | return | ㅤ |
import | ㅤ | else | select | ㅤ |
package | ㅤ | fallthrough | switch | ㅤ |
ㅤ | ㅤ | for | ㅤ | ㅤ |
对比其它语言,C 有 32,Python 有 35 个,Java 有 50 个,C++ 达到了82个。
从关键字数量上,我们就能体会到 Go 的简单了,我们几乎不需要记忆这些关键字,且关键字的组合使用也比其它语言少得多,这意味着人为创造的复杂度也比其它语言要少。
对 C/C++ 的改造
Go 的另一个特点是,身上有很浓重的 C/C++ 味道,这是因为设计团队最开始就是基于 C/C++ 设计出的 Go。
Go 语言的先祖 —— 《Go 程序语言设计》
举个例子,我们可以在 switch 身上看到 C 的影子以及 Go 的改进:
Go 去掉了每个 case 后面的 break,因为在 Go 中分支触发后会直接结束,而不是会继续执行接下来的分支,但同时 Go 也提供了
fallthrough
关键字来达到类似于 C 的效果。最小思维(minimal thought)
Go 设计者推崇“最小思维“,即一个问题仅提供一种方法去解决。这大大减少了开发人员在选择解决方案和理解他人所选方案的难度。
我们可以从以下两个方面来体会 Go 的最小思维:
- 一种代码风格:
go fmt
提供了对编程风格的规范,我们不用再为{
放在哪里来争吵了。
- 一种代码写法:Go 没有
while
,因为我们可以用for
来达到while
的效果,如果按照最小思维来思考,我们确实不需要while
这种方法。
Go is not a TMTOWTDI ("There’s More Than One Way To Do It") language. —— Best Practices for a New Go Developer https://www.cloudbees.com/blog/best-practices-for-a-new-go-developer
最小思维的另一个极端是 C++,在 C++ 中,达到目标的方法有很多种。每个开发者就会使用 C++ 的某个子集,多个开发者在一起合作的项目就会是集合的并集,如果团队没有限制使用规范,那么代码 Review 将会异常困难,一个项目里面的代码风格也会很多,因为不同开发者的使用习惯可能完全不相同。
如图所示,A、B、C、D 四位开发者各自有不同的编程风格,对于 C、D 开发者来说,理解对方的代码是困难的。
No Silver Bullet,不要妄想着设计或开发一款工具解决所有问题,那样只会让工具变得无比复杂。
组合优于继承
当我们有必要采用另一种方式处理数据时,我们应该有一些耦合程序的方式,就像花园里将浇水的软管通过预置的螺丝扣拧入另一段那样,这也是Unix IO采用的方式。——Douglas McIlroy,Unix管道的发明者(1964)
如果你熟悉 Unix,你应该使用过管道
|
,通过简单的组合就能达到强大的文本处理效果,十分简洁和高效,而 Go 也融入了组合的思想。严格来说 Go 并不算 OOP(Object-oriented programming,面向对象的编程语言),因为在 Go 中并没有继承的说法,所有的类型都是同级的。Go 提供组合的方式来解决问题,而 OOP 是通过继承来解决问题。
在语言设计层面,Go提供了正交的语法元素来给组合使用,包括:
- Go语言无类型体系(type hierarchy),类型之间是独立的,没有子类型的概念。
没有子类型意味着所有的类型都属于同一层级,
int
和 func(ResponseWriter, *Request)
都是独立的类型,没有谁是谁的子类这种情况。- 每个类型都可以有自己的方法集合,类型定义与方法实现是正交独立的。
在计算机科学中,正交(Orthogonal)指的是两个或多个概念或特性之间的独立性和相互无关性。当两个概念或特性是正交的时候,它们可以被单独处理、修改或组合,而不会对彼此产生影响。Go 语言中,类型定义与方法实现是正交的,即类型的定义和类型的方法是独立的实体。这使得我们可以为已有的类型添加新的方法,而无需修改类型的定义,从而保持代码的清晰性和可读性。
前半句很好理解,后半句什么意思呢?
就是我们可以在类型定义之后单独实现方法,也可以在已有类型上实现新的方法。
例如这里,即使已经定义了
Rectangle
类型,我们仍然可以随时在其他地方实现新的方法,而不需要修改原始类型的定义。通过这种方式,我们可以在不修改原始类型定义的情况下,为类型添加新的方法,并在代码中使用这些方法。这样的设计提供了灵活性和可扩展性,使得类型的行为可以根据需求进行扩展,同时保持代码的清晰性和可读性。
- 接口(interface)与其实现之间隐式关联。
由于类型可以实现自己的方法集合,当我们实现接口所有方法的时候,那么就是实现了这个接口,因为这个特性,我们可以轻松地实现多态和代码复用。
关于这个特性的一个深入例子:Go 适配器(Adapter)模式
- 包(package)之间是相对独立的,没有子包的概念。
什么问题是只有子包才能解决的呢?不需要,所以 Go 里面没有子包的概念。
我们看到这些类型就像一座座没有关联的“孤岛”,接下来 Go 采用了组合的方式,来将这些“孤岛”关联起来。
所谓组合
组合这种思想在设计模式中很常见,它允许对象通过将其他对象作为其成员来构建更复杂的对象结构,从而实现代码的复用和灵活性。
组合的好处包括:
- 代码复用:通过将现有的对象组合起来创建新的对象,可以避免重复编写相似的代码,提高代码复用性。
- 灵活性和可扩展性:通过组合不同的对象,可以轻松地创建具有不同功能和行为的新对象。这种灵活性使得系统更易于扩展和修改,以适应变化的需求。
- 高内聚低耦合:组合可以将相关的功能和数据封装在一起,形成高内聚的模块,同时不同模块之间的依赖关系较弱,实现了低耦合性。这样可以提高代码的可维护性和可测试性。
举一个桥接(bridge)模式的例子:
假如我们有一个形状(Shape)类, 从它能扩展出两个子类: 圆形(Circle)和方形(Square) 。
现在我们需要给形状添加红色和蓝色,假如我们只使用继承,那么我们需要创建四个子类,分别是红色圆形、红色方形、蓝色圆形、蓝色方形。光是想想就觉得很痛苦。
如果我们使用组合的方式实现,那么只需要把形状和颜色组合起来,就能达到同样的效果,在 Go 中实现也非常简洁:
这种语法叫做类型嵌入(type embedding)。
篇幅有限,如果你对设计模式感兴趣,请参考我的仓库:DesignPattern-GOhhmy27 • Updated Sep 29, 2023
Go 的组合方式
类型嵌入(Type Embedding)是一种特性,它允许一个结构体类型(被称为"外部类型")直接包含另一个结构体类型(被称为"内部类型"),从而使得内部类型的字段和方法可以被外部类型直接访问和使用。
通过类型嵌入,内部类型的字段和方法会被自动提升到外部类型中,就好像它们是外部类型自己的一样。被嵌入的类型和新类型之间没有任何关系,甚至相互完全不知道对方的存在,更没有经典 OOP 中的那种父类、子类的关系以及向上、向下转型(少了很多无聊的面试题)。
例如:
Mutex
嵌入到 poolLocal
中,poolLocal
将会拥有Mutex
类型的Lock
和Unlock
方法。我们要做的只是初始化 poolLocal
的时候传入 Mutex
的实例即可。interface 也可以进行组合,例如:
面向工程
Go 是一门以软件工程为目的的设计语言。
下面我们将通过 Rob Pike 在 2012 年的演讲稿中体会一下作者在设计 Go 时的目的和期望。
问题所在
Google 中有非常多的大规模软件,软件的代码行数以百万计,服务器软件绝大多数用的是C++,还有很多用的是Java,剩下的一部分还用到了Python。成千上万的工程师在这些代码上工作。
在软件开发过程中存在一些问题,主要包括:
- 程序构建慢
- 失控的依赖管理
- 开发人员使用编程语言的不同子集(比如 C++ 支持多范式,这样有些人用 OO,有些人用泛型)
- 代码可理解性差(代码可读性差、文档差等)
- 功能重复实现
- 升级更新消耗大
Go 的解决方案
Go 正是为了解决这些问题而设计的。Go 的目标是要消除 Google 公司软件开发中的缓慢和痛苦,从而让开发过程更加高效并且更加具有可伸缩性(scalability)。该语言的设计者和使用者都是要为大型软件系统编写、阅读和调试以及维护代码的人。
依赖处理
Go 的依赖处理是由语言定义的,通过 import 来导入需要依赖的包,这将给编译器提供一个清晰明确的依赖关系。
其次,Go 的编译器会深入处理依赖关系,如果:
- A 包 引用 B 包
- B 包 引用 C 包
- A 包 不引用 C 包
那么 Go 会按照 C ⇒ B ⇒ A 的方式编译代码。
值得一提的是,Go 不支持循环依赖,这样做的好处是给编译器提供了完整清晰的依赖关系,加快编译速度,缺点是有些功能需要重复实现,比如说:底层的网络package里有自己的整数到小数的转换程序,就是为了避免对较大的、依赖关系复杂的格式化I/O package的依赖。
语法
语法是编程语言的用户接口,它直接影响开发人员对于一门语言的使用体验。
Go语言因此在设计阶段就为语言的明确性和相关工具的编写做了考虑,设计了一套简洁的语法。与C语言家族的其他几个成员相比,Go语言的词法更为精炼,仅25个关键字(C99为37个;C++11为84个;并且数量还在持续增加)。
命名
Go 语言中,名字自己包含了可见性的信息,而不是使用常见的 private, public 等关键字来标识可见性:标识符首字母的大小写决定了可见性。
如果首字母是大写字母,这个标识符是exported(public), 否则是私有的。
同时,Go 的命名也是简洁的,你可以看到在标准库中有大量的缩写,通过缩写和上下文确定变量的含义。
常见的缩写有:
index ⇒ i
key ⇒ k
value ⇒ v
error ⇒ err
并发
Go 的并发库非常强大,channel、mutex 组合起来就能实现强大的并发能力。
工具
Go被称为“自带电池”(battery-included)的编程语言。自带电池指的是 Go 语言标准库功能丰富,多数功能无须依赖第三方包或库。Go 在标准库中提供了各类高质量且性能优良的功能包,其中的net/http、crypto/xx、encoding/xx 等包充分迎合了云原生时代关于 API/RPC Web 服务的构建需求。
工具链
Go 语言提供了十分全面、贴心的编程语言官方工具链,涵盖了编译、编辑、依赖获取、调试、测试、文档、性能剖析等的方方面面。
除了上面介绍的内容以外,Go 还有很多特性来支持它的可伸缩性(scalability),这里就不展开了。
Go 的 scalability 意味着 Go 不仅能胜任小规模的开发任务,也能在如 Google 中大规模的内部项目中游刃有余。
总结
“语言影响或决定人类的思维方式。” —— 萨丕尔-沃夫假说
在这篇文章中,我们通过三个章节来了解 Go 的设计思想和特点。
- 简单是一种美德:Go 通过语法、最小思维来实现“less is more”的效果。
- 组合优于继承中:Go 通过组合来达到低内聚、高耦合的效果。
- 面向工程:Go 通过各种各样的特性来实现 scalability。
Go 的高质量代码,必然是在 Go 的设计思想指导下编写的代码,我们在学习一门语言的时候,不仅要学习如何写出可运行的代码,更要在这基础上尝试理解语言背后的设计思想,写出符合这门语言编程思维的代码。在长期实践中,Go 开发团队、社区逐渐形成了一种编程思维,形成了符合 Go 语言哲学的惯用法(idiomatic go,类似于 Pythonic)。
希望通过这篇文章,能让大家体会到 Go 的设计思想,在之后的开发中,能写出更高质量的代码。