这是我参与「第五届青训营 」伴学笔记创作活动的第 6 天
GO interface 用法
Created: January 29, 2023 10:18 PM Tags: golang
Interface 是 Golang 中非常重要的一部分,Interface 又称接口,其功能有点类似于 Java 中的 interface,但是在一些地方完全不同于 Java 中的 interface 的设计。基于接口设计是保证代码通用性和扩展性最重要的也是最有效的方法,下面将介绍在 Golang 中 interface 的场景用法。
为什么要使用 interface
在软件设计领域,有一个很重要的架构模式是 Interface-based programing (基于接口编程),组件间的应用程序接口(API)调用可能只通过抽象化接口完成,而没有具体的类。基于这种架构,组件 A 使用组件 B 提供的功能时,只通过定义的抽象化接口,能够隐藏组件 B 的实现细节,从而在升级甚至替换组件 B 的时候,只要新的实现符合接口定义,就不会影响组件 A 的逻辑性和正确性。
通过抽象化的接口 interface,可以进一步提高模块性,将系统分割成一个个模块,相互之间只通过接口进行交互,从而将变更范围控制在单个模块内,进而提高了系统的可维护性。
GO interface 的特点
Golang 同很多语言一样,特别是 OOP 语言(例如 Java),都支持接口 Interface,来定义一些对象都具备的行为,即行为契约。但 Golang 深受 C 语言的影响,在设计理念和哲学理念上,和其他 OOP 语言是有较大差异的,具备自己的特点。
interface 定义行为
首先 interface 是一种类型,从它的定义可以看出来使用了 type 关键字,更准确的说 interface 是一种具有一组方法的类型,这些方法定义了 interface 的行为,来看个例子:
package main
import "fmt"
type Animal interface {
Eat()
Run()
}
type Dog struct {
Name string
}
func (d *Dog) Eat() {
fmt.Printf("%s is eating\n", d.Name)
}
func (d *Dog) Run() {
fmt.Printf("%s is running\n", d.Name)
}
type Cat struct {
Name string
}
func (c *Cat) Eat() {
fmt.Printf("%s is eating\n", c.Name)
}
func (c *Cat) Run() {
fmt.Printf("%s is running\n", c.Name)
}
func ShowEat(animal Animal) {
animal.Eat()
}
func ShowRun(animal Animal) {
animal.Run()
}
func main() {
dog := Dog{Name:"Kenny"}
cat := Cat{Name:"Tom"}
ShowEat(&dog)
ShowRun(&dog)
ShowEat(&cat)
ShowRun(&cat)
}
可以看到:
- 我们建立了一个 Animal 类型的 interface,它定义了 Eat() 跟 Run() 方法。
- 我们建立了一个 Dog 结构,并且实现了 interface 里面的 Eat() 和 Run() 方法。
- 还建立了 ShowEat、ShowRun方法,参数类型是 Animal。
从上面的例子中我们可以得到以下信息:
-
Animal interface 定义了 Eat() 跟 Run() 方法,在这里表示所有动物都会拥有的行为,反过来说,所有能吃会跑的东西都是 Animal 类型。这是 Go 语言类型系统的核心:
💡 我们不是以可以容纳的数据类型的形式定义我们的抽象,而是根据我们的类型可以执行的动作类设计我们的抽象。 -
任何定义了这组方法的类型都满足 Animal 接口,而且在 Go 语言中不像 Java 一样,需要 implements 关键字显式地声明实现了某个接口,Go 语言的 interface 是隐式实现的,采用了类似 duck-typing 的设计:
💡 当看到一只鸟走起来像鸭子、叫起来也像鸭子,那么这只鸟就可以被称为鸭子。 -
通过 Show Run 和 ShowEat 实现了多态的行为。所谓多态的意思是相同的信息给予不同的对象会引发不同的动作,由于参数类型是 Animal 接口,所以每个动物会有各自的吃跟跑的行为,执行出来的结果也会各不一样。
interface 类型与值
理解了如何使用 interface,下面我们再去理解 interface 到底是什么类型,或者说 interface 包含哪些部分。
我们知道go 允许不带任何方法的 interface ,这种类型的 interface 叫 empty interface。如果一个类型实现了一个 interface 中所有方法,我们说类型实现了该 interface,所以所有类型都实现了 empty interface,因为任何一种类型至少实现了 0 个方法。这意味着,如果你写了一个以 interface{} 做为入参类型,你可以给该函数任何值。所以,这个函数:
func DoSomething(v interface{}) {
// ...
}
可以接受任何参数。
这里会令人困惑:在 DoSomething 函数的内部,v 的类型是什么?我们可能认为“v 是任何类型”,但这是错误的。v 不是任何类型:它是 interface{} 类型。当给 DoSomething 函数传入一个值,如有必要,Go 运行时会进行类型转换,将一个值转换为 interface{} 值。所有的值在运行时被转换为一个类型,v 的一个静态类型是 interface{}。
这会让人怀疑,如果发生了转换,实际被传递给这个函数的是什么(或者说,存储在 []Animal 切片中的实际内容是什么)我们可以理解为,一个接口值由两个字段构成:一个字段用来指向值的底层类型的方法表,另一个字段指向保存值的实际内容。
类型断言
一个 interface 被多种类型实现时,有时候我们需要区分 interface 的变量究竟存储哪种类型的值,go 可以使用 comma, ok 的形式做区分 value, ok := em.(T):em 是 interface 类型的变量,T代表要断言的类型,value 是 interface 变量存储的值,ok 是 bool 类型表示是否为该断言的类型 T。
if t, ok := i.(*S); ok {
fmt.Println("s implements I", t)
}