Golang - Go语言中的嵌入【第二部分】:接口嵌入接口
Go 语言并不支持传统意义上的继承,相反,它提倡通过组合来扩展类型的功能。这并不是 Go 所特有的概念,继承之上的组合是 OOP 的一个众所周知的原则,在《设计模式》一书的第一章就有介绍。Embedding(嵌入)是 Go 语言一个重要的特性,有了它使得组合更加方便、更有用。虽然 Go 力求简单,但某种程度上嵌入增加了语言的复杂度,如果使用不当会导致 bug 出现。在这一系列文章中,我将介绍 Go 支持的不同种类的嵌入,并提供真实的代码示例(大部分来自 Go 语言的标准库)。
这篇文章是介绍 Go 语言支持的各种嵌入方式的系列文章的第二部分:
接口嵌入接口
在一个接口里面嵌入另一个接口,是 Go 语言里最简单的一种嵌入方式,因为接口只定义了抽象能力,并没有为类型定义新的数据和行为。
我们先看 Effective Go[1] 里列的示例,一个众所周知的 Go 标准库中嵌入接口的示例,给出 io.Reader 和 io.Writer 接口:
1 |
|
那该如何定义个接口,它既是 reader 又是 writer 呢?一种常见的方法像下面这样做:
1 |
|
除了在多个地方重复声明相同的方法,这一明显的问题之外,这种方式还降低了 ReadWriter 的可读性,因为它并没有利用组合的方式使得代码更简洁。
注意,Go 标准库中有很多类似这样的接口组合,比如:io.ReadCloser、io.WriteCloser、io.ReadWriteCloser、io.ReadSeeker、io.WriteSeeker、io.ReadWriteSeeker,其他包里面还有很多类似接口。
如果这些接口 Read 方法都重新声明的话,那恐怕得声明 10 次以上,这是很糟糕的,所幸的是接口组合可以完美地解决这个问题:
1 |
|
除了避免重复声明之外,这种方式还有一个特别明显的含义,为了实现 ReadWriter,必须先实现 Reader 和 Writer。
修复 Go1.14 里面一个接口方法覆盖的 bug
正如你期望的那样,嵌入接口是可以组合的,例如,给定接口 A、B、C 和 D:
1 |
|
接口 D 的方法结合包括:Amethod()、Bmethod()、Cmethod() 和 Dmethod()。
然而,接入接口 C 定义成下面这样:
1 |
|
按道理来说,这种定义方式不会改变 D 的方法集合。然而,在 Go1.14 版本之前,接口 D 会导致一个错误:“Duplicate method Amethod”,因为 Amethod 方法被重复声明了两次,一次是在接口 B 声明,一次是在接口 C 里声明。
Go1.14 已经修改了这个 bug[2],接口 D 的方法集包括:所有子接口的方法集和其自身的方法。
一个来自 Go 语言标准库的实际例子,io.ReadWriteCloser 接口是这样定义的:
1 |
|
但是它可以用下面这种更简洁的方式来定义:
1 |
|
在 Go 1.14 之前,这种定义方式是不可行的,因为 io.ReadCloser 和 io.WriteCloser 都定义了 Close() 方法。
示例:net.Error
net 包有声明自己的错误接口:
1 |
|
可以看到,Error 内嵌了 error 接口,这种嵌入方式表明,net.Error 也是一个 error,读代码的人一眼就能看出这点,而不需要去看 Error 的方法声明。
示例:heap.Interface
heap 包有如下的接口声明:
1 |
|
实现了 heap.Interface 接口的类型必定实现了 sort.Interface 接口,后者包括三个方法,如果不使用嵌入方式,代码就像下面这样:
1 |
|
这样一对比,嵌入式版本的写法无疑是更优的,最重要的是,可以让人一眼就能明白要想实现 heap.Interface 接口必须先实现 sort.Interface 接口。
via: https://eli.thegreenplace.net/2020/embedding-in-go-part-2-interfaces-in-interfaces/
作者:Eli Bendersky
参考资料
[1] Effective Go: https://go.dev/doc/effective_go#embedding
[2] bug: https://github.com/golang/proposal/blob/master/design/6977-overlapping-interfaces.md