接口
接口类型变量的内部表示
type iface struct {
tab *itab
data unsafe.Pointer
}
type eface struct {
_type *_type
data unsafe.Pointer
}
在运行时层面,接口类型变量有两种内部表示—— eface,iface
- eface:用于表示没有方法的空接口类型,interface{}
- iface:用于表示其余拥有方法的接口类型变量,interface
两个结构的共同点是都有两个指针字段,并且第二个指针字段功能相同,都指向当前赋值给该接口类型变量的动态类型变量的值
不同的在于eface所表示的空接口类型并无方法列表,所以指向_type结构,该结构为该接口类型变量的动态类型信息
type _type struct {
size uintptr
ptrdata uintptr
hash uint32
tflag tflag
align uint8
fieldalign uint8
kind uint8
alg *typeAlg
gcdata *byte
str nameOff
ptrToThis typeOff
}
iface除了要储存动态类型信息之外,还要存储接口本身的信息(接口的类型信息、方法列表信息等)以及动态类型所实现的方法的信息,因此iface的第一个字段指向itab结构
type itab struct {
inter *interfacetype
_type *_type
hash uint32
_ [4]byte
fun [1]uintptr
}
itab结构中第一个字段inter指向的interfacetype结构存储着该接口类型自身的信息。interfacetype类型定义如下,该interfacetype结构由类型信息(typ)、包路径名(pkgpath)和接口方法集合切片(mhdr)组成。
type interfacetype struct {
typ _type
pkgpath name
mhdr []imethod
}
eface
iface
小结
- 接口类型变量在运行时表示为eface和iface,eface用于表示空接口类型变量,iface用于表示非空接口类型变量
- 当且仅当两个接口类型变量的类型信息相同,且数据指针所指数据相同时,两个接口类型才是一致的
- 通过println可以输出接口类型变量的两部分指针变量的值
- 可通过赋值runtime包eface和iface相关类型源码,自定义输出eface/iface详尽信息的函数
- 接口类型变量的装箱操作由Go编译器和运行时共同完成
尽量定义小接口
接口越大,抽象程度越低 ————Rob Pike
接口就是将对象的行为进行抽象而形成的契约。
- 契约的自动遵守:Go中接口与其实现者之间的关系是隐式的,无须像其他语言(如Java)那样要求实现者显式放置implements声明;实现者仅需实现接口方法集中的全部方法,便算是自动遵守了契约,实现了该接口
- 小契约:契约繁了便束手束脚,降低灵活性,抑制了表现力。Go使用小契约,表现在代码上便是尽量定义小接口
小接口的优势
接口越小,抽象程度越高,被接纳度越高
计算机程序本身就是对真实世界的抽象与再构建。抽象是对同类事物去除其个别的,次要的方面,抽取其相同的,主要的方面的方法。不同的抽象程度会导致抽象出的概念对应的事物的集合不同。抽象程度越高,对应的集合空间越大,抽象程度越低(越具象,越接近事物的真实面貌)对应的集合空间越小。
易于实现和测试
在单元测试环节,构建类型去实现仅有少量方法的接口要比实现拥有较多方法的接口轻松很多。
契约职责单一,易于复用组合
Go的设计原则推崇通过组合的方式构建程序。Go开发人员一般会首先尝试通过嵌入其他已有接口类型的方式来构建新接口类型,就像通过嵌入i o.Reader和io.Writer构建io.ReadWriter那样。
定义小接口可以遵循的点
抽象出接口
要设计和定义小接口,需要先有接口。Go语言还比较年轻,其设计哲学和推崇的编程理念可能还未被广大Gopher完全理解、接纳和应用于实践当中,尤其是Go所推崇的基于接口的组合思想。尽管接口不是Go独有的,但专注于接口是编写强大而灵活的Go代码的关键。因此,在定义小接口之前,我们需要首先深入理解问题域,聚焦抽象并发现接口。
初期不要在意接口的大小,因为对问题域的理解是循序渐进的,期望在第一版代码中直接定义出小接口可能并不现实。标准库中的io.Reader和io.Writer也不是在Go刚诞生时就有的,而是在发现对网络、文件、其他字节数据处理的实现十分相似之后才抽象出来的。此外,越偏向业务层,抽象难度越高。
将大接口拆分为小接口
有了接口后,我们就会看到接口被用在代码的各个地方。一段时间后,我们来分析哪些场合使用了接口的哪些方法,是否可以将这些场合使用的接口的方法提取出来放入一个新的小接口中
一段时间后,我们发现方法1和方法2经常用在场合1中,方法3和方法4经常用在场合2中,方法5和方法6经常用在场合3中。这说明大接口1的方法呈现出一种按业务逻辑自然分组的状态。于是我们将这三组方法分别提取出来放入三个小接口中,即将大接口1拆分为三个小接口A、B和C。拆分后,原应用场合1~3使用接口1的地方可以无缝替换为使用接口A、B、C了。
接口的单一契约职责
上面拆分出的小接口是否需要进一步拆分直至每个接口都只有一个方法呢?这一点依然没有标准答案,不过大家可以考量现有小接口是否需要满足单一契约职责,就像io.Reader那样。如果需要,则可进一步拆分,提升抽象程度。
小结
- 接口是将对象的行为进行抽象而形成的契约
- Go青睐定义小接口,即方法数量1-3个、通常为1个接口
- 小接口抽象程度高,被接纳度高,易于测试和实现,易于组合复用
- 先抽象出接口,再拆分为小接口,另外接口的契约职责尽可能保持单一
使用接口作为程序水平组合的连接点
“偏好组合,正交解耦”是Go语言的重要设计哲学。
一切皆组合
- Go语言无类型体系,类型定义正交独立
- 方法和类型是正交的,每种类型都可以拥有自己的方法集合
- 接口与其实现者之间无显式关联
组合方式
- 垂直组合:通过类型嵌入机制实现垂直组合,进而实现方法实现的复用、接口定义重用等。
- 水平组合:以接口类型变量作为程序水平组合的连接点。接口是水平组合的关键,好比程序肌体上的关节,给予连接关节的两个部分或多个部分各自自由活动的能力,而整体又实现了某种功能。
以接口为连接点的水平组合
-
基本形式
func YourFuncName(param YourInterfaceType)函数/方法参数中的接口类型作为连接点,将位于多个包中的多个类型“编织”到一起,共同形成一幅程序“骨架”。同时接口类型与其依赖者之间隐式的关系满足了依赖抽象,里氏替换,接口隔离等代码设计原则。
-
包裹函数
接受接口类型参数,并返回与其参数类型相同的返回值
func YourWrapperFunc(param YourInterfaceType) YourInterfaceType通过包裹函数可以实现对输入数据的过滤、装饰、变换等操作,并将结果再次返回给调用者。
// $GOROOT/src/io/io.go func LimitReader(r Reader, n int64) Reader { return &LimitedReader{r, n} } type LimitedReader struct { R Reader N int64 } func (l *LimitedReader) Read(p []byte) (n int, err error) { ... }当采用经过LimitReader包裹后返回的io.Reader去读取内容时,读到的是经过LimitedReader约束后的内容,即只读到了原字符串前面的4字节:"hell"。由于包裹函数的返回值类型与参数类型相同,因此我们可以将多个接受同一接口类型参数的包裹函数组合成一条链来调用,其形式如
YourWrapperFunc1(YourWrapperFunc2(YourWrapperFunc3(...)) -
适配器函数类型
辅助水平组合实现的工具类型。它可以将一个满足特定函数签名的普通函数显式转换成自身类型的实例,转换后的实例同时也是某个单方法接口类型的实现者。最典型的适配器函数类型莫过于http.HandlerFunc
type Handler interface { ServeHTTP(ResponseWriter, *Request) } type HandlerFunc func(ResponseWriter, *Request) func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) { f(w, r) }
小结
- 深入理解Go的组合设计哲学
- 垂直组合可实现方法实现和接口定义的重用
- 掌握使用接口作为程序水平组合的连接点的几种形式