第二条原则: 偏好组合,正交解耦
当我们有必要采用另外一种方式处理数据时,我们应该有一些耦合程序的方式,就像花园里将浇水的软管通过预置的螺丝扣拧入另一段那样,这也是 Unix IO 采用的方式。- 道格·麦克罗伊,Unix 管道的发明者(1964)
C++、Java 等主流面向对象(以下简称 OO)语言通过庞大的、自上而下的类型体系、继承、显式接口实现等机制将程序的各个部分耦合起来,但在 Go 语言中我们找不到经典 OO 的语法元素、类型体系和继承机制,或者说 Go 语言本质上就不属于经典 OO 语言范畴。针对这种情况,很多人会问:那 Go 语言是如何将程序的各个部分有机地耦合在一起的呢?就像上面引述的道格.麦克罗伊的那句话中的浇水软管那样,Go 语言遵从的设计哲学也是组合。
在诠释组合之前,我们可以先来了解一下 Go 在语法元素设计时是如何为组合哲学的应用奠定基础的。
在 Go 语言设计层面,Go 设计者为 gopher 们提供了正交的语法元素供后续组合使用,包括:
- Go 语言无类型体系(type hierarchy),类型之间是独立的,没有子类型的概念;
- 每个类型都可以有自己的方法集合,类型定义与方法实现是正交独立的;
- 接口(interface)与其实现之间"隐式关联";
- 包(package)之间是相对独立的,没有子包的概念。
我们看到无论是包、接口还是一个个具体的类型定义(包括类型的方法集合),Go 语言为我们呈现了这样的一幅图景:一座座没有关联的“孤岛”,但每个岛内又都很精彩。现在摆在面前的工作就是在这些孤岛之间以最适当的方式建立关联(耦合),形成一个"整体"。Go 采用了组合的方式,也是唯一的方式。
Go 语言提供了的最为直观的组合的语法元素就是type embedding
,即类型嵌入。通过类型嵌入,我们可以将已经实现的功能嵌入到新类型中,以快速满足新类型的功能需求,这种方式有些类似经典 OO 的“继承”,但在原理上与经典 OO 的继承完全不同。这是一种 Go 精心设计的“语法糖”,被嵌入的类型和新类型两者之间没有任何关系,甚至相互完全不知道对方的存在,更没有经典 OO 那种父类、子类的关系以及向上、向下转型(type casting)。通过新类型实例调用方法时,方法的匹配取决于方法名字,而不是类型。这种组合方式,我称之为“垂直组合”,即通过类型嵌入,快速让一个新类型“复用”其他类型已经实现的能力,实现功能的垂直扩展。
下面是一个类型嵌入的例子:
// Go标准库:sync/pool.go
type poolLocal struct {
private interface{} // Can be used only by the respective P.
shared []interface{} // Can be used by any P.
Mutex // Protects shared.
pad [128]byte // Prevents false sharing.
}
我们在 poolLocal 这个 struct 中嵌入类型 Mutex,被嵌入的 Mutex 类型的方法集合会被提升到外面的类型中。比如,这里的 poolLocal 将拥有 Mutex 类型的 Lock 和 Unlock 方法。实际调用时,方法调用实际会被传给 poolLocal 中的 Mutex 实例。
我们在标准库中还经常看到类似如下的 interface 类型嵌入的代码:
type ReadWriter interface {
Reader
Writer
}
通过在 interface 中嵌入 interface type,实现接口行为的聚合,组成大接口,这种方式在标准库中尤为常用,并且已经成为了 Go 语言的一种常见的惯用法。
interface 是 Go 语言中真正的魔法,是 Go 语言的一个创新设计,它只是方法集合,并且它与实现者之间的关系是隐式的,它让程序内部各部分之间的耦合降至最低,同时它也是连接程序各个部分之间“纽带”。隐式的 interface 实现会不经意间满足:依赖抽象、里氏替换、接口隔离等原则,这在其他语言中是需要很"刻意"的设计谋划才能实现的,但在 Go interface 来看,一切却是自然而然的。
通过 interface 将程序内部各个部分组合在一起的方法,我这里称之为水平组合。水平组合的“模式”很多,比如:一种常见方法就是:通过接受 interface 类型参数的普通函数进行组合,例如下面代码。
func ReadAll(r io.Reader) ([]byte, error)
func Copy(dst Writer, src Reader) (written int64, err error)
ReadAll 通过 io.Reader 这个接口将 io.Reader 的实现与 ReadAll 所在的包低耦合的水平组合在一起了。类似的水平组合“模式”还有 wrapper、middleware 等,这里就不展开了,在后面讲到 interface 时再详细叙述。
此外,Go 语言内置的并发能力也可以通过组合的方式实现“对计算能力的串联”,比如:通过 goroutine+channel 的组合实现类似 Unix Pipe 的能力。
综上,组合原则的应用塑造了 Go 程序的骨架结构。类型嵌入为类型提供的垂直扩展能力,interface 是水平组合的关键,它好比程序肌体上的“关节”,给予连接“关节”的两个部分各自“自由活动”的能力,而整体上又实现了某种功能。组合也让遵循“简单”原则的 Go 语言的表现力丝毫不逊色于其他复杂的主流编程语言。