Go 数据结构 - interface
参考
Go 语言基础之接口 - 李文周
Go 语言设计与实现接口 - 面向信仰编程
Go 语言 interface - 码农桃花源
《Go 语言底层原理》第 12 章
1. 接口概述

接口定义了一种规范,提供数据传输的渠道。不论是前后端交互的接口,还是程序设计中的接口,抑或是常用的 Type-A、Type-C 结构,只有符合接口的规范设计,才能让不同组件相互连接完成数据的传输。接口可以隔离底层的实现,用户只需要知道怎么用接口就行了。
1.1 显式接口
作为后端开发领域中的大哥大,Java 需要显示声明实现的接口。
public interface MyInterface {
public String hello = "Hello";
public void sayHello();
}
public class MyInterfaceImpl implements MyInterface {
public void sayHello() {
System.out.println(MyInterface.hello);
}
}
MyInterface
接口中声明了 方法 sayHello
和 变量 hello
MyInterfaceImpl
类 使用 implements
关键字显式的声明实现了接口 MyInterface
这是一种侵入式接口设计,实现类需要明确声明自己实现了某个接口。这也意味着对接口产生了依赖,当接口被删除时程序就无法运行,耦合度较高。
1.2 隐式接口
Go 语言中接口的实现都是隐式的,只需实现接口中定义的方法,而无需显示的声明实现了该接口。
type MyInterface interface {
sayHello()
}
type MyInterfaceImpl struct{}
func (m MyInterfaceImpl) sayHello() {
fmt.Println("hello")
}
MyInterface
接口中声明了 方法 sayHello
MyInterfaceImpl
结构体实现了 方法 sayHello
,但是没有显示的声明。
这是一种非侵入式接口设计,只需要实现接口的所有方法就叫实现了该接口,即便该接口删掉了也不会影响。(鸭子类型,只要实现了鸭子叫、鸭子跑方法,就可以认定它是鸭子),耦合度很低。
2. 接口实例
2.1 空接口 & 带方法签名接口
Go 语言中接口有两种类型,带方法签名的接口和空接口。
因为目前 Go1.17
前都没有范型,空接口作为伪范型用的非常多,如 fmt
中打印函数的入参都是空接口。
func Empty(i interface{}) {
fmt.Println(i)
}
func main() {
Empty("xyz") // xyz
Empty(123) // 123
Empty(true) // true
}
下面介绍如何使用带方法签名的接口。
package main
import "fmt"
type Duck interface {
Walk()
Quack()
}
type Zhouhei struct{}
func (z Zhouhei) Walk() {
fmt.Println("周黑鸭 walk...")
}
func (z Zhouhei) Quack() {
fmt.Println("周黑鸭 quack...")
}
type Jiujiu struct{}
func (j Jiujiu) Walk() {
fmt.Println("久久鸭 walk...")
}
func (j Jiujiu) Quack() {
fmt.Println("久久鸭 quack...")
}
// Duck interface 作为
func DuckWalk(d Duck) {
d.Walk()
}
func DuckQuack(d Duck) {
d.Quack()
}
func main() {
z := Zhouhei{}
j := Jiujiu{}
DuckWalk(z) // 周黑鸭 walk...
DuckQuack(z) // 周黑鸭 quack...
DuckWalk(j) // 久久鸭 walk...
DuckQuack(j) // 久久鸭 quack...
}
上面是接口使用中很常见的例子, Duck 接口
中定义了两个方法, Zhouhei
和 Jiujiu
两个结构体实现了这两个方法,即实现了 Duck 接口
。同时 Duck 接口
作为 DuckWalk
和 DuckQuack
函数的入参,可以接收这两个结构体。
这种设计使得代码的扩展性比较高,如果再来一个结构体,只需实现接口中的方法即可通过 DuckWalk
和 DuckQuack
函数被调用。
2.2 值接收者 & 指针接收者
在结构体实现方法时,一般将结构体称为方法的接收者。接收者类型有两种: 值接收者
和 指针接收者
,上面例子中使用的就是值接收者。
// ============== 值接收 ===============
func (z Zhouhei) Walk() {
fmt.Println("周黑鸭 walk...")
}
z1 := Zhouhei{}
z2 := &Zhouhei{}
DuckWalk(z1) // 周黑鸭 walk...
DuckWalk(z2) // 周黑鸭 walk...
// ============== 指针接收 ===============
func (z *Zhouhei) Walk() {
fmt.Println("周黑鸭 walk...")
}
z1 := Zhouhei{}
z2 := &Zhouhei{}
DuckWalk(z1) // 报错
// cannot use z1 (type Zhouhei) as type Duck in argument to DuckWalk:
// Zhouhei does not implement Duck (Walk method has pointer receiver)
DuckWalk(z2) // 周黑鸭 walk...
- 当接收者为值类型:
Zhouhei
值对象和指针对象都可以赋值给该接口变量。
因为 Go 语言中有对指针类型变量求值的语法糖,通过指针可以获取到对象,指针&Zhouhei{}
内部会自动求值Zhouhei{}
- 当接收者为指针类型:值对象无法赋值给该接口变量。
因为值对象Zhouhei{}
与方法接收者 指针对象*Zhouhei
类型不一致
用值接收者还是指针接收者?
- 如果方法的接收者是值类型,无论调用者是对象还是对象指针,修改的都是对象的副本,不影响调用者;使用指针作为方法的接收者的理由:
- 方法能够修改接收者指向的值。
- 避免在每次调用方法时复制该值,在值的类型为大型结构体时,这样做会更加高效。
- 如果方法的接收者是指针类型,则调用者修改的是指针指向的对象本身。
是使用值接收者还是指针接收者,不是由该方法是否修改了调用者(也就是接收者)来决定,而是应该基于该类型的 本质
。
如果类型具备 “原始的本质”,也就是说它的成员都是由 Go 语言里内置的原始类型,如字符串,整型值等,那就 ** 定义值接收者类型的方法 **。像内置的引用类型,如 slice,map,interface,channel,这些类型比较特殊,声明他们的时候,实际上是创建了一个 header
, 对于他们也是直接定义值接收者类型的方法。这样,调用函数时,是直接 copy 了这些类型的 header
,而 header
本身就是为复制设计的。
如果类型具备非原始的本质,不能被安全地复制,这种类型总是应该被共享,那就 ** 定义指针接收者的方法 **。比如 go 源码里的文件结构体(struct File)就不应该被复制,应该只有一份 实体
。
2.3 接口类型断言
一个接口的值(简称接口值)是由 一个具体类型
和 具体类型的值
两部分组成的。这两部分分别称为接口的 动态类型
和 动态值
。

可以使用语法 i.(Type)
在运行时获取存储在接口中的动态值。其中 i 代表接口,Type 代表实现此接口的动态类型。
package main
import "fmt"
type Duck interface {
Walk()
Quack()
}
type Zhouhei struct{}
func (z Zhouhei) Walk() {
fmt.Println("周黑鸭 walk...")
}
func (z Zhouhei) Quack() {
fmt.Println("周黑鸭 quack...")
}
type Jiujiu struct{}
func (j Jiujiu) Walk() {
fmt.Println("久久鸭 walk...")
}
func main() {
var d Duck
d = Jiujiu{}
value := d.(Zhouhei)
// cannot use Jiujiu{} (type Jiujiu) as type Duck in assignment:
// Jiujiu does not implement Duck (missing Quack method)
fmt.Println(value)
}
首先声明一个接口变量作为 Jiujiu 对象的载体,然后通过类型断言语句判断是否为 Zhouhei 类型,产生错误信息:未实现 Quack 方法。
这种写法不够优雅,一般接口类型断言语句这样写: value, ok := i.(Type)
断言语句经常会和 switch
一起用
package main
import "fmt"
func check(arg interface{}) {
switch f := arg.(type) {
case bool:
fmt.Println("bool")
case string:
fmt.Println("string")
default:
fmt.Println("未识别类型", f)
}
}
func main() {
check("xyz") // string
check(true) // bool
check(123) // 未识别类型 123
}
3. 接口原理
Go 语言根据接口类型是否包含一组方法将接口类型分成了两类:
- 使用
runtime.iface
结构体表示包含方法的接口 - 使用
runtime.eface
结构体表示不包含任何方法的interface{}
类型,它的结构也相对来说比较简单,只包含指向底层数据和类型的两个指针;

data 字段
存储了接口中动态类型的函数指针。
tab 字段
存储了接口的类型、接口中的动态数据类型、动态数据类型的函数指针等。其类型为 *itab
,这也是接口的核心。

hash
是对 _type.hash
的拷贝,当我们想将 interface
类型转换成具体类型时,可以使用该字段快速判断目标类型和具体类型 runtime._type
是否一致;
fun
是一个动态大小的数组,它是一个用于动态派发的虚函数表,存储了一组函数指针。虽然该变量被声明成大小固定的数组,但是在使用时会通过原始指针获取其中的数据,所以 fun
数组中保存的元素数量是不确定的

3.1 类型结构体
itab
中的 _type 字段
代表接口存储的动态类型。Go 语言的各种数据类型都是在_type 字段的基础上通过增加额外字段来管理的。
下面是运行时包中的结构体,其中包含了很多类型的元信息,例如:类型的大小、哈希、对齐以及种类等。

size
字段存储了类型占用的内存空间,为内存空间的分配提供信息;hash
字段能够帮助我们快速确定类型是否相等;equal
字段用于判断当前类型的多个对象是否相等,该字段是为了减少 Go 语言二进制包大小从typeAlg
结构体中迁移过来的;