接口

在Go语言中接口是一种类型,一种抽象的类型。Go语言的接口有别于具体类型的概念,无论是基础数据类型,还是结构体,它类似于一种协议,而不是具体内容。

我简单的理解概括:

只要A实现了B接口里的所有方法,则代表A就是B接口类型的具体实现(不需要显示表示出A、B关系),继而可以使用多态的方式,通过B接口类型去使用A对应的方法。

接口定义

每个接口类型由一个或者多个方法签名组成

1
2
3
4
5
type 接口类型名 interface{
方法名1( 参数列表1 ) 返回值列表1
方法名2( 参数列表2 ) 返回值列表2

}

其中

  • 接口类型名一般以er作结尾,体现出接口的具体含义
  • 方法名首字母一般为大写
  • 参数列表和返回值列表中的参数变量名可以省略

实现接口

前面提到,接口就是一种协议,只要实现了接口中的所有方法就是实现了这个接口。并不需要显式的表明谁实现了谁。举个例子

1
2
3
4
5
6
7
8
9
10
11
type Sayer interface {
Say()
}

type Cat struct{}

//给Cat添加一个Say方法
func (c Cat) Say() {
fmt.Printf("喵喵喵")
}
//此时Cat便被称之为实现了Sayer接口

我们看到,代码中没有显式的表明谁实现了谁,这就是Go语言中接口与其他语言不同之处。

接口的作用

那么,接口的作用是什么呢?我们前面也提到,接口可以用来实现多态,而这个就解释了接口的作用。

举个例子:

假设在这个程序中有猫狗两种动物,它们饿了都会发出叫声,因此我们正常来说,需要写四个方法,每两个方法分别是一种动物"发出叫声"和“饿了”。这时候来了一只羊,它饿了也会发出叫声,这时候我们又需要写两种方法,这非常麻烦。

我们发现,发出叫声和饿了都是它们会做的事情,为什么不能将它归类呢,只要一个饿了和不同叫声实现就可以完成这个需求,这个就是接口的作用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
type Sayer interface { Say() }

type Cat struct{}
type Dog struct {}
type Sheep struct {}

func (c Cat) Say() { fmt.Printf("喵喵喵") }

func (d Dog) Say() { fmt.Printf("汪汪汪") }

func (s Sheep) Say() { fmt.Printf("咩咩咩") }

func MakeHunger(s Sayer) { s.Say() }

func main() {
cat := Cat{}
dog := Dog{}
sheep := Sheep{}
ani := []Sayer{cat, dog, sheep}
for _, a := range ani { MakeHunger(a) }
}

接口组合

接口与接口之间可以通过嵌套形成一个新的接口类型。对于这种由多个接口类型组合形成的新接口类型,实现了新接口类型中规定的所有方法即实现了该接口类型。go语言中的源码就有很多示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// src/io/io.go
type Reader interface {
Read(p []byte) (n int, err error)
}

type Writer interface {
Write(p []byte) (n int, err error)
}

type Closer interface {
Close() error
}

type ReadWriter interface {
Reader
Writer
}

type ReadCloser interface {
Reader
Closer
}

type WriteCloser interface {
Writer
Closer
}

接口也可以作为结构体的一个字段,可以嵌套在结构体内。

结构体内嵌接口的目的是,可以使得服务初始化时,选择接口的不同的实现方法。

例如,对于一个保存文件的服务时,需要满足一个save接口,对于save这个实现,我们具有不同的实现方法,如本地保存、oss存储等,在初始化这个保存文件服务时,我们可以选择需要的方法放进去。接下来看一个假实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
type Saver interface {
save() error
}

type SaveService struct {
Saver
}

type LocalSave struct{}

func (l LocalSave) save() error {
fmt.Printf("Save in local!\n")
return nil
}

type AliSave struct{}

func (a AliSave) save() error {
fmt.Printf("Save in AliOSS!\n")
return nil
}

func main() {
Save01 := SaveService{
new(AliSave),
}
err := Save01.save()
if err != nil {
return
}
Save02 := SaveService{
new(LocalSave),
}
err = Save02.save()
if err != nil {
return
}
}

空接口

空接口是指没有定义任何方法的接口类型。因此任何类型都可以视为实现了空接口。也正是因为空接口类型的这个特性,空接口类型的变量可以存储任意类型的值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// Any 不包含任何方法的空接口类型
type Any interface{}

// Dog 狗结构体
type Dog struct{}

func main() {
var x Any

x = "你好" // 字符串型
fmt.Printf("type:%T value:%v\n", x, x)
x = 100 // int型
fmt.Printf("type:%T value:%v\n", x, x)
x = true // 布尔型
fmt.Printf("type:%T value:%v\n", x, x)
x = Dog{} // 结构体类型
fmt.Printf("type:%T value:%v\n", x, x)
}

作用

在之前的笔记中就提到过这两个作用

空接口作为函数的参数

使用空接口实现可以接收任意类型的函数参数。

1
2
3
4
// 空接口作为函数参数
func show(a interface{}) {
fmt.Printf("type:%T value:%v\n", a, a)
}

空接口作为map的值

使用空接口实现可以保存任意值的字典。

1
2
3
4
5
6
// 空接口作为map值
var studentInfo = make(map[string]interface{})
studentInfo["name"] = "IceWindy"
studentInfo["age"] = 18
studentInfo["married"] = false
fmt.Println(studentInfo)

接口值

由于接口类型的值可以是任意一个实现了该接口的类型值,所以接口值除了需要记录具体之外,还需要记录这个值属于的类型

也就是说接口值由“类型”和“值”组成,鉴于这两部分会根据存入值的不同而发生变化,我们称之为接口的动态类型动态值

image-20230208184359551

接口值的详细理解可以看:https://www.liwenzhou.com/posts/Go/interface/#autoid-1-3-2

类型断言

类型断言是一个使用在接口值上的操作,用于检查接口类型变量所持有的值是否实现了期望的接口或者具体的类型

1
value, ok := x.(T)

x表示一个接口类型的变量,T表示一个具体类型或是接口类型。

  • valuex转化为T类型后的变量

  • ok是一个布尔值,若为true则表示断言成功,为false则表示断言失败。

类型断言的作用就很清晰了,是作为对接口类型的变量进行判断并且获取到对应的实际值。

1
2
3
4
5
6
7
8
var n Mover = &Dog{Name: "旺财"}
v, ok := n.(*Dog)
if ok {
fmt.Println("类型断言成功")
v.Name = "富贵" // 变量v是*Dog类型
} else {
fmt.Println("类型断言失败")
}

如果对一个接口值有多个实际类型需要判断,推荐使用switch语句来实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// justifyType 对传入的空接口类型变量x进行类型断言
func justifyType(x interface{}) {
switch v := x.(type) {
//关键字type只能用在switch语句里,如果用在switch外面会报错。
case string:
fmt.Printf("x is a string,value is %v\n", v)
case int:
fmt.Printf("x is a int is %v\n", v)
case bool:
fmt.Printf("x is a bool is %v\n", v)
default:
fmt.Println("unsupport type!")
}
}