2.接口
Go 语言中的接口是一组方法的签名。接口的本质是引入一个新的中间层,调用方可以通过接口与具体实现分离,解除上下游的耦合,上层的模块不再需要依赖下层的具体模块,只需要依赖一个约定好的接口。
实现上,interface 实际结构为动态类型 + 动态值,根据是否包含方法,又分为 iface
和 eface
两种,eface
多带一个函数地址列表
这种面向接口的编程方式有着非常强大的生命力,无论是在框架还是操作系统中我们都能够找到接口的身影。可移植操作系统接口(Portable Operating System Interface,POSIX)就是一个典型的例子,它定义了应用程序接口和命令行等标准,为计算机软件带来了可移植性 — 只要操作系统实现了 POSIX,计算机软件就可以直接在不同操作系统上运行。
除了解耦有依赖关系的上下游,接口还能够帮助我们隐藏底层实现,减少关注点。人能够同时处理的信息非常有限,定义良好的接口能够隔离底层的实现,让我们将重点放在当前的代码片段中。SQL 就是接口的一个例子,当我们使用 SQL 语句查询数据时,其实不需要关心底层数据库的具体实现,我们只在乎 SQL 返回的结果是否符合预期。
隐式接口
很多面向对象语言都有接口这一概念,例如 Java 和 C#。这里简单介绍一下 Java 中的接口:
Java 中的类必须通过上述方式显式地声明实现的接口,但是在 Go 语言中实现接口就不需要使用类似的方式。一个常见的 Go 语言接口是这样的:
Go 语言实现接口的方式与 Java 完全不同:
在 Java 中:实现接口需要显式地声明接口 (
implements
关键字) 并实现所有方法;在 Go 中:实现接口的所有方法就隐式地实现了接口;
我们使用上述 RPCError
结构体时并不关心它实现了哪些接口,Go 语言只会在传递参数、返回参数以及变量赋值时才会对某个类型是否实现接口进行检查,这里举几个例子来演示发生接口类型检查的时机:
Go 语言在编译期间对代码进行类型检查,上述代码总共触发了三次类型检查:
将
*RPCError
类型的变量赋值给error
类型的变量rpcErr
;将
*RPCError
类型的变量rpcErr
传递给签名中参数类型为error
的AsErr
函数;将
*RPCError
类型的变量从函数签名的返回值类型为error
的NewRPCError
函数中返回;
从类型检查的过程来看,编译器仅在需要时才检查类型,类型实现接口时只需要实现接口中的全部方法,不需要像 Java 等编程语言中一样显式声明。
数据结构
Go 语言中有两种略微不同的接口,一种是带方法的,Go语言中使用 runtime.iface
结构;一种是不带方法的,使用 runtime.eface
结构。两者都为动态类型 + 动态值,eface
还会多一个方法列表
eface (empty face)
iface
类型转换
以如下代码为例:
可以拆解为三步:
结构体
Cat
的初始化 (&Cat{Name: "draven"}`)赋值触发的类型转换过程
先构建
itab
对象,其内容包含Cat
的类型、函数方法再调用
runtime.convT2I
方法构建iface
对象,其itab
字段为上面一步的结果,data
字段为指向Cat
的指针
调用接口的方法
Quack()
其
itab
结构内的fun
字段存储了Cat
的函数索引
类型断言
switch语句生成的汇编指令会将目标类型的 hash
与接口变量中的 itab.hash
进行比较:
如果两者相等意味着变量的具体类型是
Cat
,我们会跳转到 0080 所在的分支完成类型转换。获取 SP+8 存储的
Cat
结构体指针;将结构体指针拷贝到栈顶;
调用
Quack
方法;恢复函数的栈并返回;
如果接口中存在的具体类型不是
Cat
,就会直接恢复栈指针并返回到调用方
动态派发
动态派发(Dynamic dispatch)是在运行期间选择具体多态操作(方法或者函数)执行的过程,它是面向对象语言中的常见特性6。Go 语言虽然不是严格意义上的面向对象语言,但是接口的引入为它带来了动态派发这一特性,调用接口类型的方法时,如果编译期间不能确认接口的类型,Go 语言会在运行期间决定具体调用该方法的哪个实现。
在如下所示的代码中,main
函数调用了两次 Quack
方法:
第一次以
Duck
接口类型的身份调用,调用时需要经过【运行时】的动态派发;第二次以
*Cat
具体类型的身份调用,【编译期】就会确定调用的函数:
reflect
reflect
包基本是依赖 interface
来实现的。里面定义了一个接口和一个结构体,即 reflect.Type
和 reflect.Value
,提供很多函数来获取存储在接口里的类型信息。
reflect.Type
主要提供关于类型相关的信息,所以它和 _type
关联比较紧密;reflect.Value
则结合 _type
和 data
两者,因此程序员可以获取甚至改变类型的值。
reflect
包中提供了两个基础的关于反射的函数来获取上述的接口和结构体:
调用这两个函数时,实参会先被值拷贝,转为 interface{}
类型。这样,实参的类型信息、方法集、值信息都存储到 interface{}
变量里了。
参考
Last updated