GO:2.7 接口类型的合理运用

1、正确使用接口的基础知识

1、在 Go 语言的语境中,当我们在谈论“接口”的时候,一定指的是接口类型。因为接口类型与其他数据类型不同,它是没法被实例化的

2、更具体地说,我们既不能通过调用new函数或make函数创建出一个接口类型的值,也无法用字面量来表示一个接口类型的值

3、对于某一个接口类型来说,如果没有任何数据类型可以作为它的实现,那么该接口的值就不可能存在。

4、通过关键字type和interface,我们可以声明出接口类型

5、接口类型的类型字面量与结构体类型的看起来有些相似,它们都用花括号包裹一些核心信息。只不过,结构体类型包裹的是它的字段声明,而接口类型包裹的是它的方法定义

6、注意:接口类型声明中的这些方法所代表的就是该接口的方法集合。一个接口的方法集合就是它的全部特征。

7、对于任何数据类型,只要它的方法集合中完全包含了一个接口的全部特征(即全部的方法),那么它就一定是这个接口的实现类型。比如下面这样:

1
2
3
4
5
6
type Pet interface {//声明了一个接口类型Pet
SetName(name string)
Name() string
Category() string
//包含了 3 个方法定义,方法名称分别为SetName、Name和Category,这 3 个方法共同组成了接口类型Pet的方法集合。
}

只要一个数据类型的方法集合中有这 3 个方法,那么它就一定是Pet接口的实现类型。这是一种无侵入式的接口实现方式。这种方式还有一个专有名词,叫“Duck typing”,中文常译作“鸭子类型”。

1、怎样判定一个数据类型的某一个方法实现的就是某个接口类型中的某个方法呢?

这有两个充分必要条件:
1、两个方法的签名需要完全一致
2、两个方法的名称要一模一样
例子:

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
39
40
41
package main

import "fmt"

type Pet interface {
SetName(name string)
Name() string
Category() string
}

type Dog struct {
name string // 名字。
}

func (dog *Dog) SetName(name string) {
dog.name = name
}

func (dog Dog) Name() string {
return dog.name
}

func (dog Dog) Category() string {
return "dog"
}

func main() {
// 示例1。
dog := Dog{"little pig"}
_, ok := interface{}(dog).(Pet)
fmt.Printf("Dog implements interface Pet: %v\n", ok)//false

_, ok = interface{}(&dog).(Pet)
fmt.Printf("*Dog implements interface Pet: %v\n", ok)//true
fmt.Println()

// 示例2。
var pet Pet = &dog
fmt.Printf("This pet is a %s, the name is %q.\n",
pet.Category(), pet.Name())
}

运行结果:

1
2
3
4
Dog implements interface Pet: false
*Dog implements interface Pet: true

This pet is a dog, the name is "little pig".

1、声明的类型Dog附带了 3 个方法。其中有 2 个值方法,分别是Name和Category,另外还有一个指针方法SetName。

这就意味着,Dog类型本身的方法集合中只包含了 2 个方法,也就是所有的值方法。而它的指针类型*Dog方法集合却包含了 3 个方法,

也就是说,它拥有Dog类型附带的所有值方法和指针方法。又由于这 3 个方法恰恰分别是Pet接口中某个方法的实现,所以*Dog类型就成为了Pet接口的实现类型(Dog类型不是)。

2、正因为如此,我可以声明并初始化一个Dog类型的变量dog,然后把它的指针值赋给类型为Pet的变量pet(var pet Pet = &dog)。

3、对于一个接口类型的变量来说,例如上面的变量pet,我们赋给它的值可以被叫做它的实际值(也称动态值),而该值的类型可以被叫做这个变量的实际类型(也称动态类型)。

比如,我们把取址表达式&dog的结果值赋给了变量pet,这时这个结果值就是变量pet的动态值,而此结果值的类型*Dog就是该变量的动态类型。

4、动态类型这个叫法是相对于静态类型而言的。对于变量pet来讲,它的静态类型就是Pet,并且永远是Pet,但是它的动态类型却会随着我们赋给它的动态值而变化

比如,只有我把一个Dog类型的值赋给变量pet之后,该变量的动态类型才会是Dog。如果还有一个Pet接口的实现类型Fish,并且我又把一个此类型的值赋给了pet,那么它的动态类型就会变为Fish。

还有,在我们给一个接口类型的变量赋予实际的值之前,它的动态类型是不存在的

2、当我们为一个接口变量赋值时会发生什么?

例子:把Pet接口的声明简化一下

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
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
package main

import (
"fmt"
)

type Pet interface {
Name() string
Category() string
}

type Dog struct {
name string // 名字。
}

func (dog *Dog) SetName(name string) {
dog.name = name
}

func (dog Dog) Name() string {
return dog.name
}

func (dog Dog) Category() string {
return "dog"
}

func main() {
// 示例1。
dog := Dog{"little pig"}
fmt.Printf("The dog's name is %q.\n", dog.Name())
var pet Pet = dog
dog.SetName("monster")
fmt.Printf("The dog's name is %q.\n", dog.Name())
fmt.Printf("This pet is a %s, the name is %q.\n",
pet.Category(), pet.Name())
fmt.Println()

// 示例2。
dog1 := Dog{"little pig"}
fmt.Printf("The name of first dog is %q.\n", dog1.Name())
dog2 := dog1
fmt.Printf("The name of second dog is %q.\n", dog2.Name())
dog1.name = "monster"
fmt.Printf("The name of first dog is %q.\n", dog1.Name())
fmt.Printf("The name of second dog is %q.\n", dog2.Name())
fmt.Println()

// 示例3。
dog = Dog{"little pig"}
fmt.Printf("The dog's name is %q.\n", dog.Name())
pet = &dog
dog.SetName("monster")
fmt.Printf("The dog's name is %q.\n", dog.Name())
fmt.Printf("This pet is a %s, the name is %q.\n",
pet.Category(), pet.Name())
}

运行:

1
2
3
4
5
6
7
8
9
10
11
12
The dog's name is "little pig".
The dog's name is "monster".
This pet is a dog, the name is "little pig".

The name of first dog is "little pig".
The name of second dog is "little pig".
The name of first dog is "monster".
The name of second dog is "little pig".

The dog's name is "little pig".
The dog's name is "monster".
This pet is a dog, the name is "monster".

从中去掉了Pet接口的那个名为SetName的方法(*Dog方法)。这样一来,Dog类型也就变成Pet接口的实现类型了

2、从接口类型值的存储方式和结构说起。前面说过,接口类型本身是无法被值化的。在我们赋予它实际的值之前,它的值一定会是nil,这也是它的零值。

反过来讲,一旦它被赋予了某个实现类型的值,它的值就不再是nil了。不过要注意,即使我们像前面那样把dog的值赋给了pet,pet的值与dog的值也是不同的。这不仅仅是副本与原值的那种不同

3、当我们给一个接口变量赋值的时候,该变量的动态类型会与它的动态值一起被存储在一个专用的数据结构中

严格来讲,这样一个变量的值其实是这个专用数据结构的一个实例,而不是我们赋给该变量的那个实际的值。所以我才说,pet的值与dog的值肯定是不同的,无论是从它们存储的内容,还是存储的结构上来看都是如此。不过,我们可以认为,这时pet的值中包含了dog值的副本。

把这个专用的数据结构叫做iface吧,在 Go 语言的runtime包中它其实就叫这个名字。

4、iface的实例会包含两个指针,一个是指向类型信息的指针,另一个是指向动态值的指针。这里的类型信息是由另一个专用数据结构的实例承载的,其中包含了动态值的类型,以及使它实现了接口的方法和调用它们的途径,等等。

总之,接口变量被赋予动态值的时候,存储的是包含了这个动态值的副本的一个结构更加复杂的值。

3、接口变量的值在什么情况下才真正为nil?

例子:

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
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
package main

import (
"fmt"
"reflect"
)

type Pet interface {
Name() string
Category() string
}

type Dog struct {
name string // 名字。
}

func (dog *Dog) SetName(name string) {
dog.name = name
}

func (dog Dog) Name() string {
return dog.name
}

func (dog Dog) Category() string {
return "dog"
}

func main() {
// 示例1。
var dog1 *Dog
fmt.Println("The first dog is nil.")
dog2 := dog1
fmt.Println("The second dog is nil.")
var pet Pet = dog2
if pet == nil {
fmt.Println("The pet is nil.")
} else {
fmt.Println("The pet is not nil.")
}
fmt.Printf("The type of pet is %T.\n", pet)
fmt.Printf("The type of pet is %s.\n", reflect.TypeOf(pet).String())
fmt.Printf("The type of second dog is %T.\n", dog2)
fmt.Println()

// 示例2。
wrap := func(dog *Dog) Pet {
if dog == nil {
return nil
}
return dog
}
pet = wrap(dog2)
if pet == nil {
fmt.Println("The pet is nil.")
} else {
fmt.Println("The pet is not nil.")
}
}

运行:

1
2
3
4
5
6
7
8
The first dog is nil.
The second dog is nil.
The pet is not nil.
The type of pet is *main.Dog.
The type of pet is *main.Dog.
The type of second dog is *main.Dog.

The pet is nil.

在示例1中,当我把dog2赋给Pet类型的变量pet之后,变量pet的值会是什么?

1、当我们把dog2的值赋给变量pet的时候,dog2的值会先被复制,不过由于在这里它的值是nil,所以就没必要复制了。
2、然后,Go 语言会用我上面提到的那个专用数据结构iface的实例包装这个dog2的值的副本,这里是nil。
3、虽然被包装的动态值是nil,但是pet的值却不会是nil,因为这个动态值只是pet值的一部分而已。
4、顺便说一句,这时的pet的动态类型就存在了,是*Dog。我们可以通过fmt.Printf函数和占位符%T来验证这一点,另外reflect包的TypeOf函数也可以起到类似的作用。
5、换个角度来看。我们把nil赋给了pet,但是pet的值却不是nil。

这很奇怪对吗?其实不然。在 Go 语言中,我们把由字面量nil表示的值叫做无类型的nil。这是真正的nil,因为它的类型也是nil的。虽然dog2的值是真正的nil,但是当我们把这个变量赋给pet的时候,Go 语言会把它的类型和值放在一起考虑。

也就是说,这时 Go 语言会识别出赋予pet的值是一个*Dog类型的nil。然后,Go 语言就会用一个iface的实例包装它,包装后的产物肯定就不是nil了。

只要我们把一个有类型的nil赋给接口变量,那么这个变量的值就一定不会是那个真正的nil。因此,当我们使用判等符号==判断pet是否与字面量nil相等的时候,答案一定会是false。

那么,怎样才能让一个接口变量的值真正为nil呢?
要么只声明它但不做初始化,要么直接把字面量nil赋给它

4、怎样实现接口之间的组合?

接口类型间的嵌入也被称为接口的组合。我在前面讲过结构体类型的嵌入字段,这其实就是在说结构体类型间的嵌入。

接口类型间的嵌入要更简单一些,因为它不会涉及方法间的“屏蔽”。只要组合的接口之间有同名的方法就会产生冲突,从而无法通过编译,即使同名方法的签名彼此不同也会是如此。因此,接口的组合根本不可能导致“屏蔽”现象的出现。

与结构体类型间的嵌入很相似,我们只要把一个接口类型的名称直接写到另一个接口类型的成员列表中就可以了。比如:

1
2
3
4
5
6
7
8
9
type Animal interface {
ScientificName() string
Category() string
}

type Pet interface {
Animal
Name() string
}

接口类型Pet包含了两个成员,一个是代表了另一个接口类型的Animal,一个是方法Name的定义。它们都被包含在Pet的类型声明的花括号中,并且都各自独占一行。此时,Animal接口包含的所有方法也就成为了Pet接口的方法。

Go 语言团队鼓励我们声明体量较小的接口,并建议我们通过这种接口间的组合来扩展程序、增加程序的灵活性。

这是因为相比于包含很多方法的大接口而言,小接口可以更加专注地表达某一种能力或某一类特征,同时也更容易被组合在一起。

Go 语言标准库代码包io中的ReadWriteCloser接口和ReadWriter接口就是这样的例子,它们都是由若干个小接口组合而成的。以io.ReadWriteCloser接口为例,它是由io.Reader、io.Writer和io.Closer这三个接口组成的。

这三个接口都只包含了一个方法,是典型的小接口。它们中的每一个都只代表了一种能力,分别是读出、写入和关闭。我们编写这几个小接口的实现类型通常都会很容易。并且,一旦我们同时实现了它们,就等于实现了它们的组合接口io.ReadWriteCloser。

即使我们只实现了io.Reader和io.Writer,那么也等同于实现了io.ReadWriter接口,因为后者就是前两个接口组成的。可以看到,这几个io包中的接口共同组成了一个接口矩阵。它们既相互关联又独立存在。

一个能够体现接口组合优势的小例子:

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
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
package main

import (
"fmt"
)

type Animal interface {
// ScientificName 用于获取动物的学名。
ScientificName() string
// Category 用于获取动物的基本分类。
Category() string
}

type Named interface {
// Name 用于获取名字。
Name() string
}

type Pet interface {
Animal
Named
}

type PetTag struct {
name string
owner string
}

func (pt PetTag) Name() string {
return pt.name
}

func (pt PetTag) Owner() string {
return pt.owner
}

type Dog struct {
PetTag
scientificName string
}

func (dog Dog) ScientificName() string {
return dog.scientificName
}

func (dog Dog) Category() string {
return "dog"
}

func main() {
petTag := PetTag{name: "little pig"}
_, ok := interface{}(petTag).(Named)
fmt.Printf("PetTag implements interface Named: %v\n", ok)
dog := Dog{
PetTag: petTag,
scientificName: "Labrador Retriever",
}
_, ok = interface{}(dog).(Animal)
fmt.Printf("Dog implements interface Animal: %v\n", ok)
_, ok = interface{}(dog).(Named)
fmt.Printf("Dog implements interface Named: %v\n", ok)
_, ok = interface{}(dog).(Pet)
fmt.Printf("Dog implements interface Pet: %v\n", ok)
}

运行:

1
2
3
4
PetTag implements interface Named: true
Dog implements interface Animal: true
Dog implements interface Named: true
Dog implements interface Pet: true

总之,善用接口组合和小接口可以让你的程序框架更加稳定和灵活。