1. 面向接口编程
1.1 特征
面向接口编程,强调的是模块之间通过接口进行交互。首先,调用方指定一组方法签名,然后,由被调用方实现这组方法。接口编程与其他编程显著不同的一点是,接口编程关注的是方法,而不是属性。在很多的编程场景中,方法是围绕属性进行定义的。如下图:但在接口编程中,恰好相反,方法处于核心位置,而属性可以自由定义,进行扩展。在不同的数据结构上,实现同一个接口。如下图:此外,从文字上也可以看到,面向接口编程是以接口为中心的编程范式。
1.2 优势
- 模块解耦
通过接口抽象,明确模块之间的依赖关系。模块与模块之间的边界更加清晰,各个模块也更加独立。
- 可维护性
Interface 隐藏了具体实现,各个模块只需要保持 Interface 不变,内部逻辑可以自由重构。
- 易扩展、易升级
只需要实现相同的一组方法,就可以很方面地对模块进行扩展、升级。
- 易测试
在写单元测试时,需要屏蔽外部依赖。而 Interface 非常容易 Mock 。面向接口编程的代码,更加容易编写单元测试。
2. Go 中的 Interface
Go 中的 Interface 是一种聚合了一组方法的类型。
2.1 声明和实现
通过 interface
关键字,加上一组方法签名,就可以声明一个接口。方法签名指的是,方法的名字、参数、返回值等能够唯一确定一个方法的全部信息。下面声明一个 Animal 的接口:
|
|
Go 中的 Interface 是隐式实现的。不需要指定继承了哪一个 Interface ,只需要在同一个 package 中,某个类型实现了 Interface 的全部方法,就称这个类型为 Interface 的实现。下面这个例子的 Cat 类型实现了 Animal 接口:
|
|
在使用时,可以声明一个 Aminal 类型的变量,指向 Cat之后,调用接口中的方法。
|
|
2.2 Receiver 类型
Go 中可以定义 type 上的方法,Receiver 指的就是这个方法接受的类型,例如示例中的 Cat 。上面的示例中,使用 i = &Cat{}
和 i = Cat{}
都是可以的,Go 会自动实现转换。除了 Receiver 为指针类型,而赋值为值类型。因为 Go 采用的是值传递,赋值之后,取得的地址指向的是拷贝的地址,而不是预期的数据地址,编译时会报错:
|
|
内置的类型不能作为 Receiver 。Receiver 分为两种类型,值和指针,也可以混合使用。
- 值类型的 Receiver
|
|
- 指针类型的 Receiver
|
|
在实现接口时,会遇到这两种类型的选择。传递指针,似乎更加高效,但也意味着操作更危险。在不需要修改数据的场景中,应该尽量采用值 Receiver 。
2.3 空接口
空接口的特殊性在于,interface{}
可以接受任意类型的值。这个特性看着像是动态语言才有的,但 Go 是一个静态语言。在编译时,Go 会对 Interface 进行严格校验。
|
|
空接口在接受或者返回不确定类型参数时,非常有用。但不确定的类型,也会带来维护的成本。在项目中,我们应该尽量避免使用空接口,以增强项目的可维护性。
2.4 类型判断
由于 interface{} 可以接受任意类型的值,在程序运行过程中,很有可能需要知道 Interface 的动态值类型。有两种方法,可以判断类型:
- 断言
|
|
- switch
|
|
需要注意的是,这里的 Cat 和 Dog 值对 Animal 做类型判断时,为 true 。
3. Interface 的组合
|
|
Interface 中,还可以嵌套其他 Interface 。实现这个 Interface 的类型,需要同时提供全部 Interface 定义的方法。这种技巧在项目中,非常有用。