Please enable Javascript to view the contents

一起来学 Go --(6)Interface

 ·  ☕ 3 分钟

1. 面向接口编程

1.1 特征

面向接口编程,强调的是模块之间通过接口进行交互。首先,调用方指定一组方法签名,然后,由被调用方实现这组方法。

接口编程与其他编程显著不同的一点是,接口编程关注的是方法,而不是属性。在很多的编程场景中,方法是围绕属性进行定义的。如下图:

但在接口编程中,恰好相反,方法处于核心位置,而属性可以自由定义,进行扩展。在不同的数据结构上,实现同一个接口。如下图:

此外,从文字上也可以看到,面向接口编程是以接口为中心的编程范式。

1.2 优势

  • 模块解耦

通过接口抽象,明确模块之间的依赖关系。模块与模块之间的边界更加清晰,各个模块也更加独立。

  • 可维护性

Interface 隐藏了具体实现,各个模块只需要保持 Interface 不变,内部逻辑可以自由重构。

  • 易扩展、易升级

只需要实现相同的一组方法,就可以很方面地对模块进行扩展、升级。

  • 易测试

在写单元测试时,需要屏蔽外部依赖。而 Interface 非常容易 Mock 。面向接口编程的代码,更加容易编写单元测试。

2. Go 中的 Interface

Go 中的 Interface 是一种聚合了一组方法的类型。

2.1 声明和实现

通过 interface 关键字,加上一组方法签名,就可以声明一个接口。方法签名指的是,方法的名字、参数、返回值等能够唯一确定一个方法的全部信息。下面声明一个 Animal 的接口:

1
2
3
type Animal interface{
	Call()
}

Go 中的 Interface 是隐式实现的。不需要指定继承了哪一个 Interface ,只需要在同一个 package 中,某个类型实现了 Interface 的全部方法,就称这个类型为 Interface 的实现。下面这个例子的 Cat 类型实现了 Animal 接口:

1
2
3
4
5
type Cat struct {}

func (c Cat) Call() {
	// do something
}

在使用时,可以声明一个 Aminal 类型的变量,指向 Cat之后,调用接口中的方法。

1
2
3
var i Animal
i = &Cat{}
i.Call()

2.2 Receiver 类型

Go 中可以定义 type 上的方法,Receiver 指的就是这个方法接受的类型,例如示例中的 Cat 。

上面的示例中,使用 i = &Cat{}i = Cat{} 都是可以的,Go 会自动实现转换。除了 Receiver 为指针类型,而赋值为值类型。因为 Go 采用的是值传递,赋值之后,取得的地址指向的是拷贝的地址,而不是预期的数据地址,编译时会报错:

1
	Cat does not implement Animal (Call method has pointer receiver)

内置的类型不能作为 Receiver 。Receiver 分为两种类型,值和指针,也可以混合使用。

  • 值类型的 Receiver
1
2
3
4
5
6
7
type Dog struct {
	times int
}

func (d Dog) Call() {
	d.times = d.times + 1 // not work
}
  • 指针类型的 Receiver
1
2
3
4
5
6
7
type Dog struct {
	times int
}

func (d *Dog) Call() {
	d.times = d.times + 1 // work
}

在实现接口时,会遇到这两种类型的选择。传递指针,似乎更加高效,但也意味着操作更危险。在不需要修改数据的场景中,应该尽量采用值 Receiver 。

2.3 空接口

空接口的特殊性在于,interface{} 可以接受任意类型的值。这个特性看着像是动态语言才有的,但 Go 是一个静态语言。在编译时,Go 会对 Interface 进行严格校验。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
package main

import "fmt"

func main() {
	var i interface{}
	i = 123
	fmt.Println(i)
	i = "123"
	fmt.Println(i)
}

空接口在接受或者返回不确定类型参数时,非常有用。但不确定的类型,也会带来维护的成本。在项目中,我们应该尽量避免使用空接口,以增强项目的可维护性。

2.4 类型判断

由于 interface{} 可以接受任意类型的值,在程序运行过程中,很有可能需要知道 Interface 的动态值类型。有两种方法,可以判断类型:

  • 断言
1
2
3
4
5
6
7
var i interface{}
i = Cat{}
if _, ok := i.(Cat); ok {
	fmt.Println("i is Cat ")
} else {
	fmt.Println("i is not Cat ")
}
  • switch
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
var i interface{}
i = Dog{}
switch i.(type) {
case Cat:
	fmt.Println("i is Cat")
case Dog:
	fmt.Println("i is Dog")
default:
	fmt.Println("i is unknow")
}

需要注意的是,这里的 Cat 和 Dog 值对 Animal 做类型判断时,为 true 。

3. Interface 的组合

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
type Active interface{
	Eat()
}

type Animal interface{
	Active
	Call()
}

type Cat struct {}

func (c Cat) Call() {}

func (c *Cat) Eat() {}

Interface 中,还可以嵌套其他 Interface 。实现这个 Interface 的类型,需要同时提供全部 Interface 定义的方法。这种技巧在项目中,非常有用。


微信公众号
作者
微信公众号