一起来学 Go (6)Interface

2023年 1月 4日 20.0k 0

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 定义的方法。这种技巧在项目中,非常有用。

相关文章

KubeSphere 部署向量数据库 Milvus 实战指南
探索 Kubernetes 持久化存储之 Longhorn 初窥门径
征服 Docker 镜像访问限制!KubeSphere v3.4.1 成功部署全攻略
那些年在 Terraform 上吃到的糖和踩过的坑
无需 Kubernetes 测试 Kubernetes 网络实现
Kubernetes v1.31 中的移除和主要变更

发布评论