接口
在Go中,接口值可以用来包裹非接口值;然后,通过值包裹,反射和多态得以实现。
自从1.18版本开始,Go已经支持自定义泛型。 在自定义泛型中,接口类型总可以被用做类型约束。 事实上,所有的类型约束都是接口类型。在Go 1.18版本之前,所有的接口类型均可用做值类型。 但是从Go 1.18版本开始,有些接口类型只能被用做类型约束。 可被用做值类型的接口类型称为基本接口类型。
即接口分为值接口类型和类型约束接口类型。
本文主要讲述的是基本接口类型。
接口类型介绍和类型集(Type Set)
一个接口类型定义了一些类型条件。 所有满足了全部这些条件的非接口类型形成了一个类型集合。 此类型集合称为此接口类型的类型集。
接口类型是通过内嵌若干接口元素来定义类型条件的。 目前(Go 1.22)支持两种接口元素:方法元素和类型元素。
一个方法元素呈现为一个方法描述(method specification)。 内嵌在接口类型中的方法描述不能使用空标识符
_
命名。一个类型元素可以是一个类型名称、一个类型字面表示形式、一个近似类型或者一个类型并集。 本文不过多介绍后两者。对于前两者,也只谈及当它们表示接口类型的情况。
举个例子,预声明的error
接口类型的定义如下。 它内嵌了一个方法描述Error() string
。 在此定义中,interface{...}
称为接口类型的字面表示形式,其中interface
为一个关键字。
type error interface {
Error() string
}
我们可以说此error
接口类型(直接)指定了一个方法(描述):Error() string
。 它的类型集由所有拥有此同样描述的方法的非接口类型组成。 理论上,此类型集是一个无限集。当然对于一个具体的Go项目,此集合是有限的。
下面是一些其它接口类型定义和别名声明。
// 此接口直接指定了两个方法和内嵌了两个其它接口。
// 其中一个为类型名称,另一个为类型字面表示形式。
type ReadWriteCloser = interface {
Read(buf []byte) (n int, err error)
Write(buf []byte) (n int, err error)
error // 一个类型名称
interface{ Close() error } // 一个类型字面表示形式
}
// 此接口类型内嵌了一个近似类型。
// 它的类型集由所有底层类型为[]byte的类型组成。
type AnyByteSlice = interface {
~[]byte
}
// 此接口类型内嵌了一个类型并集。它的类型集包含6个类型:
// uint、uint8、uint16、uint32、uint64和uintptr。
type Unsigned interface {
uint | uint8 | uint16 | uint32 | uint64 | uintptr
}
将一个接口类型(无论呈现为类型名称还是类型字面表示形式)内嵌到另一个接口类型中等价于将前者中的元素(递归)展开放入后者。 比如,别名ReadWriteCloser
表示的接口类型等价于下面这个类型字面表示形式表示的直接指定了4个方法的接口类型。
interface {
Read(buf []byte) (n int, err error)
Write(buf []byte) (n int, err error)
Error() string
Close() error
}
上面这个接口类型(即别名ReadWriteCloser
表示的接口类型)的类型集由所有拥有全部这4个指定方法的非接口类型组成。 从理论上,这也是一个无限集。它肯定是error
接口类型的类型集的子集。
请注意:在Go 1.18之前,只有接口类型名称可以内嵌在接口类型中。
下面的代码片段中展示的接口类型都称为空接口类型。它们什么也没有内嵌。
// 一个无名空接口类型。
interface{}
// Nothing是一个定义空接口类型。
type Nothing interface{}
事实上,Go 1.18引入了一个预声明的类型别名any
,用来表示空接口类型interface{}
。
一个空接口类型的类型集由所有由非接口类型组成。
类型的方法集
每个类型有一个方法集。
- 对于一个非接口类型,它的方法集由为此类型(无论显式还是隐式)声明所有方法的方法描述组成。
- 对于一个接口类型,它的方法集由此接口类型(无论直接还是间接)指定的所有方法描述组成。
对于前文中提到的接口类型,
- 别名
ReadWriteCloser
表示的接口类型的方法集包含4个方法(描述)。 - 预声明的
error
接口类型的方法集包含一个方法(描述)。 - 一个空接口类型的方法集为空。
基本接口类型
基本接口类型是指可以用做值类型的接口类型。 一个非基本接口类型只能为用做(自定义泛型中使用的)约束接口类型(即类型约束)。
目前(Go 1.22),每一个基本接口类型都可以使用一个方法集来完全定义。 换句话说,一个基本接口类型不需要内嵌任何类型元素。
注意:内嵌了接口类型和接口字面值本质上也是使用了方法集,因为是递归展开放入接口中的。
在前文中的例子中,别名ReadWriteCloser
表示的接口类型为一个基本接口类型, 但是Unsigned
接口类型和别名AnyByteSlice
表示的接口类型均不是基本接口类型。 后两者均只能用做约束接口类型。
空接口类型和预声明的error
接口类型也都是基本接口类型。
如果两个无名基本接口类型的方法集是相同的,则这两个类型肯定为同一个类型。 但是请注意:不同代码包中的同名非导出方法名将总被认为是不同名的。
匿名接口(无名接口)
无名的接口类型(即匿名接口)可以在多个场景中运用到。以下是一些常见的使用场景:
匿名参数:匿名接口类型可以用于函数参数,允许函数接收任何实现了该接口的类型。
func TestInterface(t *testing.T) {
e := errors.New("this is an error")
printError(e)
}
func printError(a interface {
Error() string
}) {
fmt.Println(a.(error).Error())
}
结构体字段:匿名接口类型可以作为结构体的字段类型,以便该字段可以保存任何实现了该接口的值。
type interfaceStruct struct {
e interface{
Error() string
}
}
临时使用:在需要临时使用某个接口但不需要命名接口类型的情况下,可以使用匿名接口类型。
类型实现
如果一个非接口类型处于一个接口类型的类型集中,则我们说此非接口类型实现了此接口类型。 如果一个接口类型的类型集是另一个接口类型的类型集的子集,则我们说前者实现了后者。
因为一个类型集的总是它自己的子集,一个接口类型总是实现了它自己。 类似地,如果两个接口类型的类型集相同,则它们相互实现了对方。 事实上,两个拥有相同类型集的无名接口类型为同一个接口类型。
如果一个(接口或者非接口)类型T
实现了一个接口类型X
,那么类型T
的方法集肯定是接口类型X
的方法集的超集。
在Go中,实现关系是隐式的。 两个类型之间的实现关系不需要在代码中显式地表示出来。 Go中没有类似于implements
的关键字。 Go编译器将自动在需要的时候检查两个类型之间的实现关系。
隐式实现关系的设计使得一个声明在另一个代码包(包括标准库包)中的类型可以被动地实现一个用户代码包中的接口类型。 比如,如果我们声明一个像下面这样的接口类型,则database/sql
标准库包中声明的DB
和Tx
类型都实现了这个接口类型,因为它们都拥有此接口类型指定的三个方法。
import "database/sql"
...
type DatabaseStorer interface {
Exec(query string, args ...interface{}) (sql.Result, error)
Prepare(query string) (*sql.Stmt, error)
Query(query string, args ...interface{}) (*sql.Rows, error)
}
接口类型的内部定义
我们可以认为接口类型在内部是如下定义的:
type _interface struct {
dynamicType *_type // 引用着接口值的动态类型
dynamicValue unsafe.Pointer // 引用着接口值的动态值
}
一个接口类型含有两个指针字段。 每个非零接口值的(两个)间接部分分别存储着此接口值的动态类型和动态值。 这两个间接部分被此接口值的直接字段dynamicType
和dynamicValue
所引用。
上面这个内部定义只用于表示空接口类型的值。空接口类型没有指定任何方法。非空接口类型的内部定义如下:
type _interface struct {
dynamicTypeInfo *struct {
dynamicType *_type // 引用着接口值的动态类型
methods []*_function // 引用着动态类型的对应方法列表
}
dynamicValue unsafe.Pointer // 引用着动态值
}
一个非空接口类型的值的dynamicTypeInfo
字段的methods
字段引用着一个方法列表。 此列表中的每一项为此接口值的动态类型上定义的一个方法,此方法对应着此接口类型所指定的一个的同描述的方法。
值包裹
目前(Go 1.22),接口值的类型必须为一个基本接口类型。 在本文余下的内容里,当一个值类型被提及,此值类型可能是一个非接口类型,也可能是一个基本接口类型,但它肯定不是一个非基本接口类型。
每个接口值都可以看作是一个用来包裹一个非接口值的盒子。 欲将一个非接口值包裹在一个接口值中,此非接口值的类型必须实现了此接口值的类型。
在Go中,如果类型T
实现了一个(基本)接口类型I
,则类型T
的值都可以隐式转换到类型I
。 换句话说,类型T
的值可以赋给类型I
的可修改值。 当一个T
值被转换到类型I
(或者赋给一个I
值)的时候,
- 如果类型
T
是一个非接口类型,则此T
值的一个复制将被包裹在结果(或者目标)I
值中。 此操作的时间复杂度为O(n)
,其中n
为T
值的尺寸。 - 如果类型
T
也为一个接口类型,则此T
值中当前包裹的(非接口)值将被复制一份到结果(或者目标)I
值中。 官方标准编译器为此操作做了优化,使得此操作的时间复杂度为O(1)
,而不是O(n)
。
包裹在一个接口值中的非接口值的类型信息也和此非接口值一起被包裹在此接口值中(见下面详解)。
当一个非接口值被包裹在一个接口值中,此非接口值称为此接口值的动态值,此非接口值的类型称为此接口值的动态类型。
接口值的动态值的直接部分是不可修改的,除非它的动态值被整体替换为另一个动态值。
接口类型的零值也用预声明的nil
标识符来表示。一个nil接口值中什么也没包裹。将一个接口值修改为nil将清空包裹在此接口值中的非接口值。
(注意,在Go中,很多其它非接口类型的零值也使用nil
标识符来表示。 非接口类型的nil零值也可以被包裹在接口值中。 一个包裹了一个nil非接口值的接口值不是一个nil接口值,因为它并非什么都没包裹。)
也就是接口值的零值是指动态类型和动态值都为 nil。当仅且当这两部分的值都为 nil 的情况下,这个接口值就才会被认为 接口值 == nil。
因为任何类型都实现了空接口类型,所以任何非接口值都可以被包裹在任何一个空接口类型的接口值中。 (以后,一个空接口类型的接口值将被称为一个空接口值。注意空接口值和nil接口值是两个不同的概念。) 因为这个原因,空接口值可以被认为是很多其它语言中的any
类型。
当一个类型不确定值(除了类型不确定的nil
)被转换为一个空接口类型(或者赋给一个空接口值),此类型不确定值将首先转换为它的默认类型。 (或者说,此类型不确定值将被推断为一个它的默认类型的类型确定值。)
在编译时刻,Go编译器将构建一个全局表用来存储代码中要用到的各个类型的信息。 对于一个类型来说,这些信息包括:此类型的种类(kind)、此类型的所有方法和字段信息、此类型的尺寸,等等。 这个全局表将在程序启动的时候被加载到内存中。
在运行时刻,当一个非接口值被包裹到一个接口值,Go运行时(至少对于官方标准运行时来说)将分析和构建这两个值的类型的实现关系信息,并将此实现关系信息存入到此接口值内。 对每一对这样的类型,它们的实现关系信息将仅被最多构建一次。并且为了程序效率考虑,此实现关系信息将被缓存在内存中的一个全局映射中,以备后用。 所以此全局映射中的条目数永不减少。 事实上,一个非零接口值在内部只是使用一个指针字段来引用着此全局映射中的一个实现关系信息条目。
对于一个非接口类型和接口类型对,它们的实现关系信息包括两部分的内容:
- 动态类型(即此非接口类型)的信息。
- 一个方法表(切片类型),其中存储了所有此接口类型指定的并且为此非接口类型(动态类型)声明的方法。
这两部分的内容对于实现Go中的两个特性起着至关重要的作用。
- 动态类型信息是实现反射的关键。
- 方法表是实现多态的关键。
多态
当非接口类型T
的一个值t
被包裹在接口类型I
的一个接口值i
中,通过i
调用接口类型I
指定的一个方法时,事实上为非接口类型T
声明的对应方法将通过非接口值t
被调用。 换句话说,调用一个接口值的方法实际上将调用此接口值的动态值的对应方法。 比如,当方法i.m
被调用时,其实被调用的是方法t.m
。 一个接口值可以通过包裹不同动态类型的动态值来表现出各种不同的行为,这称为多态。
当方法i.m
被调用时,i
存储的实现关系信息的方法表中的方法t.m
将被找到并被调用。 此方法表是一个切片,所以此寻找过程只不过是一个切片元素访问操作,不会消耗很多时间。
注意,在nil接口值上调用方法将产生一个panic,因为没有具体的方法可被调用。
除了上述这个好处,多态也使得一个代码包的开发者可以在此代码包中声明一个接口类型并声明一个拥有此接口类型参数的函数(或者方法),从而此代码包的一个用户可以在用户包中声明一个实现了此接口类型的用户类型,并且将此用户类型的值做为实参传递给此代码包中声明的函数(或者方法)的调用。 此代码包的开发者并不用关心一个用户类型具体是如何声明的,只要此用户类型满足此代码包中声明的接口类型规定的行为即可。
反射
一个接口值中存储的动态类型信息可以被用来检视此接口值的动态值和操纵此动态值所引用的值。 这称为反射。