一、概念
Go 语言中的接口是一组方法的签名,它是 Go 语言的重要组成部分。接口的本质是引入一个新的中间层,调用方可以通过接口与具体实现分离,解除上下游的耦合,上层的模块不再需要依赖下层的具体模块,只需要依赖一个约定好的接口。
即我们只需要关注怎么使用,而不需要关注具体实现。
计算机科学中的接口是比较抽象的概念,但是编程语言中接口的概念就更加具体。Go 语言中的接口是一种内置的类型,它定义了一组方法的签名。
二、定义接口
在 Go 中通过 interface
关键词来定义接口,在接口中我们只能定义方法签名,不能包含成员变量,例如:
type error interface {
Error() string
}
2.1 实现
若我们需要实现某个接口,我们只需要实现它定义的方法签名即可:
type RPCError struct {
Code int64
Message string
}
func (e *RPCError) Error() string {
return fmt.Sprintf("%s, code=%d", e.Message, e.Code)
}
提示
在 Go 语言中接口的实现都是隐式的,我们只需要实现接口的方法就是实现了对应的接口。
2.2 类型
接口是 Go 中的一种类型,它能够出现在变量的定义、函数的入参和返回值中并对它们进行约束,不过 Go 语言中有两种略微不同的接口,一种是带有一组方法的接口,另一种是不带任何方法的 interface{}
:
runtime.iface
是第一种接口,runtime.eface
表示第二种不包含任何方法的接口 interface{}
,两种接口虽然都使用 interface
声明,但是由于后者在 Go 语言中很常见,所以在实现时使用了特殊的类型。
package main
func main() {
type Test struct{}
v := Test{}
Print(v)
}
func Print(v interface{}) {
println(v)
}
注意
interface{}
类型不是任意类型,在调用 Print
函数时会对参数 v 进行类型转换,将原来的 Test
类型转换成 interface{}
类型。
2.3 指针和接口
当使用指针实现接口时,只有指针变量才会实现该接口;使用结构体实现接口时,指针类型和结构体类型都会实现该接口。
type Duck interface {
Quack()
}
type Cat struct{}
func (c *Cat) Quack() {
fmt.Println("meow")
}
func main() {
var c Duck = Cat{}
c.Quack()
}
主要原因是 Go 函数传递参数是值传递,对于指针则是拷贝一个新的指针,但是指向还是原结构体,所以可以隐式的对变量解引用获取指针。
三、数据结构
type eface struct { // 16 字节
_type *_type
data unsafe.Pointer
}
type iface struct { // 16 字节
tab *itab
data unsafe.Pointer
}
3.1 类型结构体 _type
runtime._type
是 Go 运行时表现:
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 二进制包的大小。
3.2 结构体 itab
runtime.itab
结构体是接口类型的核心组成部分,占用 32 字节,是接口类型和具体类型的组合:
type itab struct { // 32 字节
inter *interfacetype
_type *_type
hash uint32
_ [4]byte
fun [1]uintptr
}
inter
和 _type
用来表示字段类型
hash
是 _type.hash
的拷贝,可以使用该字段来快速转换为具体类型或者用于类型判断。
fun
是一个动态大小的数组,它是一个用于动态派发的虚函数表,存储了一组函数指针。虽然该变量被声明成大小固定的数组,但是在使用时会通过原始指针获取其中的数据,所以 fun
数组中保存的元素数量是不确定的
四、具体类型和接口类型相互转换
4.1 结构体指针
对于结构体指针转为接口类型时,底层使用 runtime.iface
结构体表示。其中 itab
用来表示结构体与接口的关系,data
指针则是指向数据的指针。
调用接口类型对应的方法时,需要去调用具体实现的方法,这种行为属于动态派发的方法调用;为了减少额外的开销 Go 编译时会自动转换为对目标方法的直接调用,例如:Duck.Quack()
编译期间自动转换为 *Cat.Quack()
。
4.2 结构体类型
同结构体指针,还是一样将结构体转换成对应的接口的类型,然后调用接口的方法。具体过程:
1、初始化结构体
2、将结构体类型转换为接口类型
通过 runtime.convT2I
函数将结构体转换成 iface
类型的接口,调用接口方法时就是调用 runtime.itab
中实现对应方法的指针。
4.3 类型断言
通过类型断言,我们可以将一个接口类型转换成具体类型:
非空接口 iface
func main() {
var c Duck = &Cat{Name: "draven"}
switch c.(type) {
case *Cat:
cat := c.(*Cat)
cat.Quack()
}
}
通过比较目标类型的 hash
与接口变量的 itab.hash
若相等直接进行类型转换,然后调用具体的实现。
空接口 eface
func main() {
var c interface{} = &Cat{Name: "draven"}
switch c.(type) {
case *Cat:
cat := c.(*Cat)
cat.Quack()
}
}
转换逻辑与 iface
同理。
五、多态派发
动态派发(Dynamic dispatch)是在运行期间选择具体多态操作(方法或者函数)执行的过程,它是面向对象语言中的常见特性。如果编译期间不能确认接口的类型,Go 语言会在运行期间决定具体调用该方法的哪个实现。
func main() {
var c Duck = &Cat{Name: "draven"}
c.Quack()
c.(*Cat).Quack()
}
使用结构体类型实现接口比结构体指针类型实现接口开销高,我们应当尽量避免使用结构体类型实现接口。