Golang学习 - 接口(interface)
hanpy

interface 是 Go 语言的基础特性之一,可以理解为一种类型的规范或者约定。

接口的声明

1
2
3
// 空接口
type interfaceName interface {
}

接口指定了类型应该具有的方法,类型决定了如何实现这些方法。

1
2
3
4
5
// 有方法列表的接口
type PeopleInface interface {
GetName() string
GetAge() int
}

接口的实现

Go语言中,接口的实现是隐式的。不用明确地指出某一个类型实现了某一个接口,只要在某一类型的方法中实现了接口中的全部方法签名,就意味着此类型实现了这一接口。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
type PeopleInface interface {
GetName() string
GetAge() int
}

type Student struct {
Name string
Age int
}

func (s Student) GetName() string {
return s.Name
}

func (s Student) GetAge() int {
return s.Age
}

接口动态类型

存储在接口变量中的类型称为接口的动态类型,而将接口本身的类型称为接口的静态类型

下面的代码中,sp是接口变量,sp的动态类型是student,静态类型是speaksp的静态类型永远是speak,动态类型却会随着我们赋给它的动态值而变

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package main

import "fmt"

type speak interface {
sayHello()
}

type student struct {
Name string
}

func (s student) sayHello() {
fmt.Println("sayHello")
}

func main() {
var sp speak
stu := student{"zhangsan"}
sp = stu
}

接口动态调用

当接口变量中存储了具体的动态类型时,可以调用接口中所有的方法。
在对接口变量进行动态调用时,调用的方法只能是接口中具有的方法。

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
type Speak interface {
sayHello()
}

type Student struct{}

func (s Student) sayHello() {
fmt.Println("Student SayHello")
}

type Teacher struct{}

func (t Teacher) sayHello() {
fmt.Println("Teacher SayHello")
}

func (t Teacher) sayGoodBy() {
fmt.Println("Teacher SayGoodBy")
}

func main() {
var s Speak

// 接口变量存储了不同的动态类型,接口动态 调用表现出不同动态类型的行为
s = Student{}
s.sayHello() // Student SayHello

s = Teacher{}
s.sayHello() // Teacher SayHello

// sayGoodBy()是接口之外的方法就是不能调用的 | 未解析的引用 'sayGoodBy'
s.sayGoodBy()
}

指针和接口

在实现接口的时候有两种方式,结构体实现和指针实现

《Go语言设计与实现》中的图

image

结构体类型和指针类型是不同的,在实现接口时这两种类型也不能划等号。虽然两种类型不同,但是上图中的两种实现不可以同时存在,Go 语言的编译器会在结构体类型和指针类型都实现一个方法时报错 “method redeclared”。


实现接口时可以选择接受者的类型,即结构体或者结构体指针,在初始化时也可以初始化成结构体或者指针
在初始化成结构体的时候,是可以调用接受者是结构体结构体指针的方法。
在初始化成指针的时候,只可以调用接受者是结构体指针的方法。

……这特么的看代码吧!

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
type Duck interface {
Walk()
Quack()
}

type Cat struct {
}

func (c Cat) Walk() {
fmt.Println("catWalk")
}

func (c *Cat) Quack() {
fmt.Println("catMeow")
}

func main() {
// 这种情况是不能编译成功的
// cannot use Cat{} (type Cat) as type Duck in assignment:
// Cat does not implement Duck (Quack method has pointer receiver)
// 结构体类型就没有实现Duck接口
var c Duck = Cat{}
c.Walk()
c.Quack()

// 这种情况是可以通过编译的
// 当我们使用结构体实现接口时,指针类型和结构体类型都会实现该接口
// Walk方法接受者虽然是结构体,但是会隐式的有一个接受者是指针的实现
var c1 Duck = &Cat{}
c1.Walk()
c1.Quack()
}

至于为什么这样,在《Go语言设计与实现》中有这样的解释

image

两种接口的数据结构

Go 语言根据接口类型是否包含一组方法将接口类型分成了两类:

  • 使用 runtime.iface 结构体表示包含方法的接口
  • 使用 runtime.eface 结构体表示不包含任何方法的 interface{} 类型

eface结构体

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
type eface struct { // 16 字节
_type *_type
data unsafe.Pointer
}

type _type struct {
size uintptr
ptrdata uintptr
hash uint32
tflag tflag
align uint8
fieldAlign uint8
kind uint8
equal func(unsafe.Pointer, unsafe.Pointer) bool
gcdata *byte
str nameOff
ptrToThis typeOff
}
  • size 字段存储了类型占用的内存空间,为内存空间的分配提供信息;
  • hash 字段能够帮助我们快速确定类型是否相等;
  • equal 字段用于判断当前类型的多个对象是否相等,该字段是为了减少 Go 语言二进制包大小从 typeAlg 结构体中迁移过来的

iface结构体

runtime.itab 结构体是接口类型的核心组成部分,每一个 runtime.itab 都占 32 字节,我们可以将其看成接口类型和具体类型的组合,它们分别用 inter_type 两个字段表示:

1
2
3
4
5
6
7
type itab struct { // 32 字节
inter *interfacetype
_type *_type
hash uint32
_ [4]byte
fun [1]uintptr
}
  • hash 是对 _type.hash 的拷贝,当我们想将 interface 类型转换成具体类型时,可以使用该字段快速判断目标类型和具体类型 runtime._type 是否一致
  • fun 是一个动态大小的数组,它是一个用于动态派发的虚函数表,存储了一组函数指针。虽然该变量被声明成大小固定的数组,但是在使用时会通过原始指针获取其中的数据,所以 fun 数组中保存的元素数量是不确定的