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