GO接口学习笔记

接口简介

接口是什么

接口是一种特殊的类型,本身不与特定实现绑定,是对给它赋值的自定义类型的一种抽象和约束。

人话:接口类型定义了几个方法,其他类型需要实现这些方法才能把值传给它

接口用来做什么

引入中间层,调用与具体实现分离,解除上下游的耦合。

人话:接口与自定义类型不再绑定,接口的使用者只关心方法的使用,而底层的实现交给自定义类型。

引申:计算机中很多都用到了接口的概念, 比如linux的虚拟文件系统对上层提供统一的读写接口,屏蔽了底层不同文件系统的差异。

接口的使用

基本使用

Go语言接口声明如下:

1
2
3
4
5
// 类型关键字 接口名 接口关键字
type People interface {
// 函数名(参数列表)返回参数列表
Say(s string) error
}

实现接口的自定义类型如下

1
2
3
4
5
6
type XiaoMing struct{}

func (x XiaoMing) Say(s string) error {
fmt.Println("hello ", s)
return nil
}

Go语言接口是隐式实现的,不需要指明具体实现的接口,如下为具体执行代码:

1
2
3
4
func main() {
var p People = XiaoMing{}
p.Say("XiaoMing") // output: hello XiaoMing
}

接收者

接口本身只是个方法集,没有具体实现,需要被其他自定义类型来实现它定义的方法,这些方法在使用过程中分为两种情况:值接收和指针接收,以下就是对这两种情况的介绍。

值接受者

是什么

表现形式如下:

1
2
3
4
5
6
7
8
9
10
11
type XiaoMing struct{
Name string
}
func (x XiaoMing) Say(s string) {
fmt.Println(s, x.Name)
return nil
}
func main() {
var p People = Xiaoming{Name:"xiaoming"}
p.Say("hello,") // hello,xiaoming
}

上面代码仍旧是我们之前的例子,接收者就是方法的调用者,值接受者的调用者本身就是值,接收者的取名是针对x而言,p的完整空间赋值给了x,x是p的一份拷贝,对x的任何修改不会影响到p。

什么时候用

当我们不需要对源数据进行修改,同时也希望避免修改源数据时选择值接受者,新增一份源数据的拷贝。

指针接收者

是什么

表现形式如下:

1
2
3
4
func(x *XiaoMing) Say(s string) error{
fmt.Println(s, x.Name)
return nil
}

上面代码不一样的是对接收者类型变成了*XiaoMing,这样对于方法来说,会申请一块空间存贮调用者的指针,虽然使用的时候还是x.Name,但实际是go内部给我们做的转换,实际执行的是(*x).Name

什么时候用

这种情况传过来的值是源数据的地址,则我们对x.Name做的修改实际是对源数据进行的修改,当我们需要对源数据修改的时候使用这种情况。

总结

可以理解为方法的接收者就是函数的第一个参数,这样就清楚很多了,go只有值传递,指针接收者只是传过来的值就是指向源数据的内存地址,所以我们可以对源数据进行修改,如果源数据数据量大,可以考虑使用指针传递,避免数据大量拷贝,但是要记得尽量避免对数据的非法修改。

类型断言

类型断言是作用于接口上的一种操作,主要用来判断接口与我们断言的类型是否匹配,语法如下:

1
2
s := x.(T) 			// 检查失败会抛出panic
s, ok := x.(T) // 进行安全检查,不会触发panic,ok判断是否成功断言

s的值是断言后的类型值。

除了以上常用情况,go还提供了一种类型判断的使用方法,用来判断当前接口的具体类型:

1
2
3
4
5
6
switch x.(type) {
case string:
...
case int:
...
}

#接口的底层实现

类型系统介绍

go语言中包含一天类型系统,包含int、string等内置类型,以及我们使用type定义的自定义类型,每种类型在go语言中都有自己的类型元数据,在类型元数据中有一部分是大家都有的属性,就放在runtime._type结构体中,如下所示:

1
2
3
4
5
6
7
8
9
10
type _type struct {
size uintptr
ptrdata uintptr // size of memory prefix holding all pointers
hash uint32
tflag tflag
align uint8
fieldAlign uint8
kind uint8
...
}

_type在类型元数据中是作为head存在的,之后是其他描述信息,比如slice类型元数据如下:

1
2
3
4
type slicetype struct {
typ _type // head
elem *_type // 其他描述信息,指向存储类型的元数据,比如[]string类型的slice这里就是指向string类型元数据
}

作为自定义类型,是可以有自己方法的,这种情况下除了head和其他描述信息外还有一个uncommontype结构体:

1
2
3
4
5
6
7
type uncommontype struct {
pkgpath nameOff // 类型所在的包路径
mcount uint16 // 该类型关联了多少方法
xcount uint16 // 多少个导出的方法
moff uint32 // 方法元数据组成的数组距离uncommontype偏移了多少字节(可以找到方法数组)
_ uint32 // unused
}

有个moff这个字段,我们就可以通过计算偏移地址找到方法数组,调用具体方法了。

每种类型的元数据都是唯一的。

最后,介绍一种写法:

1
2
type a = int32 	// a和int32指向同一个类型元数据,a只是别名(比如rune和int32)
type b int32 // b在这里是一种新的类型,b和int32有各自的类型元数据

接口的数据结构

空接口的数据结构最简单

1
2
3
4
type eface struct {
_type *_type // 指向类型
data unsafe.Pointer // 指向值
}

他只需要知道数据类型以及具体值的内存地址就可以了。

非空接口的数据结构如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
type iface struct {
tab *itab
data unsafe.Pointer
}

type itab struct {
inter *interfacetype // 接口类型
_type *_type // 动态类型元数据
hash uint32 // copy of _type.hash. Used for type switches.
_ [4]byte
fun [1]uintptr // variable sized. fun[0]==0 means _type does not implement inter.
}

type interfacetype struct {
typ _type
pkgpath name
mhdr []imethod // 接口的要求实现的方法列表
}

需要注意的是itab.fun这个变量保存着类型实现接口要求的函数列表的首地址,以方便能快速找到需要的方法。

由于非空类型对应的具体类型是在运行时分配的,itab内的值也都是变化的,因此go在内部会缓存一个key-value的哈希表,key是<接口类型,动态类型>的组合,value则为itab的指针,这样可以快速找到对应的itab缓存。

引用文献

-------------本文结束,感谢您的阅读-------------