Go 语言中的接口是一组方法的签名。接口的本质是引入一个新的中间层,调用方可以通过接口与具体实现分离,解除上下游的耦合,上层的模块不再需要依赖下层的具体模块,只需要依赖一个约定好的接口。
实现上,interface 实际结构为动态类型 + 动态值,根据是否包含方法,又分为 iface
和 eface
两种,eface
多带一个函数地址列表
这种面向接口的编程方式有着非常强大的生命力,无论是在框架还是操作系统中我们都能够找到接口的身影。可移植操作系统接口(Portable Operating System Interface,POSIX)就是一个典型的例子,它定义了应用程序接口和命令行等标准,为计算机软件带来了可移植性 — 只要操作系统实现了 POSIX,计算机软件就可以直接在不同操作系统上运行。
除了解耦有依赖关系的上下游,接口还能够帮助我们隐藏底层实现,减少关注点。人能够同时处理的信息非常有限,定义良好的接口能够隔离底层的实现,让我们将重点放在当前的代码片段中。SQL 就是接口的一个例子,当我们使用 SQL 语句查询数据时,其实不需要关心底层数据库的具体实现,我们只在乎 SQL 返回的结果是否符合预期。
很多面向对象语言都有接口这一概念,例如 Java 和 C#。这里简单介绍一下 Java 中的接口:
// 接口定义
public interface MyInterface {
public void sayHello();
}
// 实现接口的类
public class MyInterfaceImpl implements MyInterface {
public void sayHello() {
System.out.println("Hello");
}
}
Java 中的类必须通过上述方式显式地声明实现的接口,但是在 Go 语言中实现接口就不需要使用类似的方式。一个常见的 Go 语言接口是这样的:
// 接口定义
type error interface {
Error() string
}
// 实现接口的类
type RPCError struct {
Code int64
Message string
}
func (e *RPCError) Error() string {
return fmt.Sprintf("%s, code=%d", e.Message, e.Code)
}
Go 语言实现接口的方式与 Java 完全不同:
- 在 Java 中:实现接口需要显式地声明接口 (
implements
关键字) 并实现所有方法; - 在 Go 中:实现接口的所有方法就隐式地实现了接口;
我们使用上述 RPCError
结构体时并不关心它实现了哪些接口,Go 语言只会在传递参数、返回参数以及变量赋值时才会对某个类型是否实现接口进行检查,这里举几个例子来演示发生接口类型检查的时机:
func main() {
var rpcErr error = NewRPCError(400, "unknown err") // typecheck1
err := AsErr(rpcErr) // typecheck2
println(err)
}
func NewRPCError(code int64, msg string) error {
return &RPCError{ // typecheck3
Code: code,
Message: msg,
}
}
func AsErr(err error) error {
return err
}
Go 语言在编译期间对代码进行类型检查,上述代码总共触发了三次类型检查:
- 将
*RPCError
类型的变量赋值给error
类型的变量rpcErr
; - 将
*RPCError
类型的变量rpcErr
传递给签名中参数类型为error
的AsErr
函数; - 将
*RPCError
类型的变量从函数签名的返回值类型为error
的NewRPCError
函数中返回;
从类型检查的过程来看,编译器仅在需要时才检查类型,类型实现接口时只需要实现接口中的全部方法,不需要像 Java 等编程语言中一样显式声明。
Go 语言中有两种略微不同的接口,一种是带方法的,Go语言中使用 runtime.iface
结构;一种是不带方法的,使用 runtime.eface
结构。两者都为动态类型 + 动态值,eface
还会多一个方法列表
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 // 判断当前类型的多个对象是否相等,该字段是为了减少 Go 语言二进制包大小从 typeAlg 结构体中迁移过来的
gcdata *byte
str nameOff
ptrToThis typeOff
}
// runtime/runtime2.go
type iface struct { // 16 字节
tab *itab
data unsafe.Pointer
}
type itab struct { // 32 字节
inter *interfacetype // 接口定义的类型信息
_type *_type // 接口实际指向值的类型信息
hash uint32 // 用于快速判断目标类型和具体类型 runtime._type 是否一致
_ [4]byte
fun [1]uintptr // 接口方法实现列表,即函数地址列表,按字典序排序
}
// runtime/type.go
type interfacetype struct {
typ _type
pkgpath name
mhdr []imethod // 接口方法声明列表,按字典序排序
}
// 接口的方法声明
type imethod struct {
name nameOff // 方法名
ityp typeOff // 描述方法参数返回值等细节
}
以如下代码为例:
var c Duck = &Cat{Name: "draven"}
c.Quack()
可以拆解为三步:
- 结构体
Cat
的初始化 (&Cat{Name: "draven"}`) - 赋值触发的类型转换过程
- 先构建
itab
对象,其内容包含Cat
的类型、函数方法 - 再调用
runtime.convT2I
方法构建iface
对象,其itab
字段为上面一步的结果,data
字段为指向Cat
的指针
- 先构建
- 调用接口的方法
Quack()
- 其
itab
结构内的fun
字段存储了Cat
的函数索引
- 其
func main() {
var c Duck = &Cat{Name: "draven"}
switch c.(type) {
case *Cat:
cat := c.(*Cat)
cat.Quack()
}
}
00058 CMPL go.itab.*"".Cat,"".Duck+16(SB), $593696792
;; if (c.tab.hash != 593696792) {
00068 JEQ 80 ;;
00070 MOVQ 24(SP), BP ;; BP = SP+24
00075 ADDQ $32, SP ;; SP += 32
00079 RET ;; return
;; } else {
00080 LEAQ ""..autotmp_4+8(SP), AX ;; AX = &Cat{Name: "draven"}
00085 MOVQ AX, (SP) ;; SP = AX
00089 CALL "".(*Cat).Quack(SB) ;; SP.Quack()
00094 JMP 70 ;; ...
;; BP = SP+24
;; SP += 32
;; return
;; }
switch语句生成的汇编指令会将目标类型的 hash
与接口变量中的 itab.hash
进行比较:
-
如果两者相等意味着变量的具体类型是
Cat
,我们会跳转到 0080 所在的分支完成类型转换。- 获取 SP+8 存储的
Cat
结构体指针; - 将结构体指针拷贝到栈顶;
- 调用
Quack
方法; - 恢复函数的栈并返回;
- 获取 SP+8 存储的
-
如果接口中存在的具体类型不是
Cat
,就会直接恢复栈指针并返回到调用方
动态派发(Dynamic dispatch)是在运行期间选择具体多态操作(方法或者函数)执行的过程,它是面向对象语言中的常见特性6。Go 语言虽然不是严格意义上的面向对象语言,但是接口的引入为它带来了动态派发这一特性,调用接口类型的方法时,如果编译期间不能确认接口的类型,Go 语言会在运行期间决定具体调用该方法的哪个实现。
在如下所示的代码中,main
函数调用了两次 Quack
方法:
- 第一次以
Duck
接口类型的身份调用,调用时需要经过【运行时】的动态派发; - 第二次以
*Cat
具体类型的身份调用,【编译期】就会确定调用的函数:
func main() {
var c Duck = &Cat{Name: "draven"}
c.Quack()
c.(*Cat).Quack()
}
reflect
包基本是依赖 interface
来实现的。里面定义了一个接口和一个结构体,即 reflect.Type
和 reflect.Value
,提供很多函数来获取存储在接口里的类型信息。
reflect.Type
主要提供关于类型相关的信息,所以它和 _type
关联比较紧密;reflect.Value
则结合 _type
和 data
两者,因此程序员可以获取甚至改变类型的值。
reflect
包中提供了两个基础的关于反射的函数来获取上述的接口和结构体:
func TypeOf(i interface{}) Type
func ValueOf(i interface{}) Value
调用这两个函数时,实参会先被值拷贝,转为 interface{}
类型。这样,实参的类型信息、方法集、值信息都存储到 interface{}
变量里了。