变得复杂的接口
有时候使用泛型编程时,我们会书写长长的类型约束,如下:
// 一个可以容纳所有int,uint以及浮点类型的泛型切片
type Slice[T int | int8 | int16 | int32 | int64 | uint | uint8 | uint16 | uint32 | uint64 | float32 | float64] []T
理所当然,这种写法是我们无法忍受也难以维护的,而 Go 支持将类型约束单独拿出来定义到接口中,从而让代码更容易维护:
type IntUintFloat interface {
int | int8 | int16 | int32 | int64 | uint | uint8 | uint16 | uint32 | uint64 | float32 | float64
}
type Slice[T IntUintFloat] []T
这段代码把类型约束给单独拿出来,写入了接口类型 IntUintFloat
当中。需要指定类型约束的时候直接使用接口 IntUintFloat
即可。
不过这样的代码依旧不好维护,而接口和接口、接口和普通类型之间也是可以通过 |
进行组合:
type Int interface {
int | int8 | int16 | int32 | int64
}
type Uint interface {
uint | uint8 | uint16 | uint32
}
type Float interface {
float32 | float64
}
type Slice[T Int | Uint | Float] []T // 使用 '|' 将多个接口类型组合
上面的代码中,我们分别定义了 Int
, Uint
, Float
三个接口类型,并最终在 Slice[T]
的类型约束中通过使用 |
将它们组合到一起。
同时,在接口里也能直接组合其他接口,所以还可以像下面这样:
type SliceElement interface {
Int | Uint | Float | string // 组合了三个接口类型并额外增加了一个 string 类型
}
type Slice[T SliceElement] []T
~ : 指定底层类型
var s1 Slice[int] // 正确
type MyInt int
var s2 Slice[MyInt] // ✗ 错误。MyInt类型底层类型是int但并不是int类型,不符合 Slice[T] 的类型约束
这里发生错误的原因是,泛型类型 Slice[T]
允许的是 int
作为类型实参,而不是 MyInt
(虽然 MyInt
类型底层类型是 int
,但它依旧不是 int
类型)。
为了从根本上解决这个问题,Go 新增了一个符号 ~
,在类型约束中使用类似 ~int
这种写法的话,就代表着不光是 int
,所有以 int
为底层类型的类型也都可用于实例化。
使用 ~
对代码进行改写之后如下:
type Int interface {
~int | ~int8 | ~int16 | ~int32 | ~int64
}
type Uint interface {
~uint | ~uint8 | ~uint16 | ~uint32
}
type Float interface {
~float32 | ~float64
}
type Slice[T Int | Uint | Float] []T
var s Slice[int] // 正确
type MyInt int
var s2 Slice[MyInt] // MyInt底层类型是int,所以可以用于实例化
type MyMyInt MyInt
var s3 Slice[MyMyInt] // 正确。MyMyInt 虽然基于 MyInt ,但底层类型也是int,所以也能用于实例化
type MyFloat32 float32 // 正确
var s4 Slice[MyFloat32]
限制:使用 ~
时有一定的限制:
-
~
后面的类型不能为接口 -
~
后面的类型必须为基本类型
type MyInt int
type _ interface {
~[]byte // 正确
~MyInt // 错误,~后的类型必须为基本类型
~error // 错误,~后的类型不能为接口
}
从方法集(Method set)到类型集(Type set)
上面的例子中,我们学习到了一种接口的全新写法,而这种写法在 Go1.18 之前是不存在的。如果你比较敏锐的话,一定会隐约认识到这种写法的改变这也一定意味着 Go 语言中 接口(interface)
这个概念发生了非常大的变化。
是的,在 Go1.18 之前,Go 官方对 接口(interface)
的定义是:接口是一个方法集(method set)。
An interface type specifies a method set called its interface. |
就如下面这个代码一样,ReadWriter
接口定义了一个接口(方法集),这个集合中包含了 Read()
和 Write()
这两个方法。所有同时定义了这两种方法的类型被视为实现了这一接口。
type ReadWriter interface {
Read(p []byte) (n int, err error)
Write(p []byte) (n int, err error)
}
但是,我们如果换一个角度来重新思考上面这个接口的话,会发现接口的定义实际上还能这样理解:
我们可以把ReaderWriter
接口看成代表了一个 类型的集合,所有实现了Read() Writer()
这两个方法的类型都在接口代表的类型集合当中。
通过换个角度看待接口,在我们眼中接口的定义就从 方法集(method set)
变为了 类型集(type set)
。而 Go1.18 开始就是依据这一点将接口的定义正式更改为了 类型集(Type set)。
An interface type defines a type set (一个接口类型定义了一个类型集)
你或许会觉得,这不就是改了下概念上的定义实际上没什么用吗?是的,如果接口功能没变化的话确实如此。但是还记得下面这种用接口来简化类型约束的写法吗:
type Float interface {
~float32 | ~float64
}
type Slice[T Float] []T
这就体现出了为什么要更改接口的定义了。用 类型集 的概念重新理解上面的代码的话就是:
接口类型Float
代表了一个 类型集合,所有以float32
或float64
为底层类型的类型,都在这一类型集之中。
而 type Slice[T Float] []T
中,类型约束 的真正意思是:
类型约束 指定了类型形参可接受的类型集合,只有属于这个集合中的类型才能替换形参用于实例化。
如:
var s Slice[int] // int 属于类型集 Float ,所以int可以作为类型实参
var s Slice[chan int] // chan int 类型不在类型集 Float 中,所以错误
接口实现(implement)定义的变化
既然接口定义发生了变化,那么从 Go1.18 开始 接口实现(implement)
的定义自然也发生了变化:
当满足以下条件时,我们可以说 类型 T 实现了接口 I ( type T implements interface I):
-
T
不是接口时:类型T
是接口I
代表的类型集中的一个成员 (T is an element of the type set of I) -
T
是接口时:T
接口代表的类型集是I
代表的类型集的子集(Type set of T is a subset of the type set of I)
类型的并集
并集我们已经很熟悉了,之前一直使用的 |
符号就是求类型的并集( union
)
type Uint interface { // 类型集 Uint 是 ~uint 和 ~uint8 等类型的并集
~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64
}
类型的交集
接口可以不止书写一行,如果一个接口有多行类型定义,那么取它们之间的 交集 。
type AllInt interface {
~int | ~int8 | ~int16 | ~int32 | ~int64 | ~uint | ~uint8 | ~uint16 | ~uint32 | ~uint32
}
type Uint interface {
~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64
}
type A interface { // 接口A代表的类型集是 AllInt 和 Uint 的交集
AllInt
Uint
}
type B interface { // 接口B代表的类型集是 AllInt 和 ~int 的交集
AllInt
~int
}
上面这个例子中
-
接口 A 代表的是 AllInt 与 Uint 的 交集,即
~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64
-
接口 B 代表的则是 AllInt 和
~int
的 交集,即~int
除了上面的交集,下面也是一种交集:
type C interface {
~int
int
}
很显然,~int
和 int
的交集只有 int
一种类型,所以接口 C 代表的类型集中只有 int
一种类型。
空集
当多个类型的交集如下面 Bad
这样为空的时候,Bad
这个接口代表的类型集为一个 空集:
type Bad interface {
int
float32
} // 类型 int 和 float32 没有相交的类型,所以接口 Bad 代表的类型集为空
没有任何一种类型属于空集
。虽然 Bad
这样的写法是可以编译的,但实际上并没有什么意义。
空接口和 any
上面说了空集,接下来说一个特殊的类型集——空接口 interface{}
。因为,Go1.18 开始接口的定义发生了改变,所以 interface{}
的定义也发生了一些变更:
空接口代表了所有类型的集合。
所以,对于 Go1.18 之后的空接口应该这样理解:
-
虽然空接口内没有写入任何的类型,但它代表的是所有类型的集合,而非一个 空集
-
类型约束中指定 空接口 的意思是指定了一个包含所有类型的类型集,并不是类型约束限定了只能使用 空接口 来做类型形参。
// 空接口代表所有类型的集合。写入类型约束意味着所有类型都可拿来做类型实参 type Slice[T interface{}] []T var s1 Slice[int] // 正确 var s2 Slice[map[string]string] // 正确 var s3 Slice[chan int] // 正确 var s4 Slice[interface{}] // 正确
因为空接口是一个包含了所有类型的类型集,所以我们经常会用到它。于是,Go1.18 开始提供了一个和空接口 interface{}
等价的新关键词 any
,用来使代码更简单:
type Slice[T any] []T // 代码等价于 type Slice[T interface{}] []T
实际上 any
的定义就位于 Go 语言的 builtin.go
文件中(参考如下), any
实际上就是 interaface{}
的别名(alias),两者完全等价:
// any is an alias for interface{} and is equivalent to interface{} in all ways.
type any = interface{}
所以从 Go 1.18 开始,所有可以用到空接口的地方其实都可以直接替换为 any
,如:
var s []any // 等价于 var s []interface{}
var m map[string]any // 等价于 var m map[string]interface{}
func MyPrint(value any){
fmt.Println(value)
}
如果你高兴的话,项目迁移到 Go1.18 之后可以使用下面这行命令直接把整个项目中的空接口全都替换成 any。当然因为并不强制,所以到底是用 interface{}
还是 any
全看自己喜好。
gofmt -w -r 'interface{} -> any' ./...
Go 语言项目中就曾经有人提出过把 Go 语言中所有 |
comparable(可比较) 和 可排序(ordered)
对于一些数据类型,我们需要在类型约束中限制只接受能 !=
和 ==
对比的类型,如 map
:
// 错误。因为 map 中键的类型必须是可进行 != 和 == 比较的类型
type MyMap[KEY any, VALUE any] map[KEY]VALUE
所以 Go 直接内置了一个叫 comparable
的接口,它代表了所有可用 !=
以及 ==
对比的类型:
type MyMap[KEY comparable, VALUE any] map[KEY]VALUE // 正确
comparable
比较容易引起误解的一点是很多人容易把他与可排序搞混淆。可比较指的是 可以执行 !=
、==
操作的类型,并没确保这个类型可以执行大小比较( >
,<
,<=
,>=
)。如下:
type OhMyStruct struct {
a int
}
var a, b OhMyStruct
a == b // 正确。结构体可使用 == 进行比较
a != b // 正确
a > b // 错误。结构体不可比大小
而可进行大小比较的类型被称为 Orderd
。目前 Go 语言并没有像 comparable
这样直接内置对应的关键词,所以想要的话需要自己来定义相关接口,比如我们可以参考 Go 官方包 golang.org/x/exp/constraints
如何定义:
// Ordered 代表所有可比大小排序的类型
type Ordered interface {
Integer | Float | ~string
}
type Integer interface {
Signed | Unsigned
}
type Signed interface {
~int | ~int8 | ~int16 | ~int32 | ~int64
}
type Unsigned interface {
~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 | ~uintptr
}
type Float interface {
~float32 | ~float64
}
这里虽然可以直接使用官方包 golang.org/x/exp/constraints ,但因为这个包属于实验性质的 |
接口两种类型
我们接下来再观察一个例子,这个例子是阐述接口是类型集最好的例子:
type ReadWriter interface {
~string | ~[]rune
Read(p []byte) (n int, err error)
Write(p []byte) (n int, err error)
}
最开始看到这一例子你一定有点懵不太理解它代表的意思,但是没关系,我们用类型集的概念就能比较轻松理解这个接口的意思:
接口类型ReadWriter
代表了一个类型集合,所有以string
或[]rune
为底层类型,并且实现了Read()
、Write()
这两个方法的类型都在ReadWriter
代表的类型集当中。
如下面代码中,StringReadWriter
存在于接口 ReadWriter
代表的类型集中,而 BytesReadWriter
因为底层类型是 []byte
(既不是 string
也是不 []rune
) ,所以它不属于 ReadWriter
代表的类型集。
// 类型 StringReadWriter 实现了接口 Readwriter
type StringReadWriter string
func (s StringReadWriter) Read(p []byte) (n int, err error) {
// ...
}
func (s StringReadWriter) Write(p []byte) (n int, err error) {
// ...
}
// 类型BytesReadWriter 没有实现接口 Readwriter
type BytesReadWriter []byte
func (s BytesReadWriter) Read(p []byte) (n int, err error) {
...
}
func (s BytesReadWriter) Write(p []byte) (n int, err error) {
...
}
你一定会说,这接口也变得太复杂了把,那我定义一个 ReadWriter
类型的接口变量,然后接口变量赋值的时候不光要考虑到方法的实现,还必须考虑到具体底层类型?心智负担也太大了吧。是的,为了解决这个问题也为了保持 Go 语言的兼容性,Go1.18 开始将接口分为了两种类型
-
基本接口(Basic interface)
-
一般接口(General interface)
基本接口(Basic interface)
接口定义中如果只有方法的话,那么这种接口被称为 基本接口(Basic interface)。这种接口就是 Go1.18 之前的接口,用法也基本和 Go1.18 之前保持一致。基本接口大致可以用于如下几个地方:
-
最常用的,定义接口变量并赋值
type MyError interface { // 接口中只有方法,所以是基本接口 Error() string } // 用法和 Go1.18之前保持一致 var err MyError = fmt.Errorf("hello world")
-
基本接口因为也代表了一个类型集,所以也可用在类型约束中
// io.Reader 和 io.Writer 都是基本接口,也可以用在类型约束中 type MySlice[T io.Reader | io.Writer] []Slice
一般接口(General interface)
如果接口内不光只有方法,还有类型的话,这种接口被称为 一般接口(General interface) ,如下例子都是一般接口:
type Uint interface { // 接口 Uint 中有类型,所以是一般接口
~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64
}
type ReadWriter interface { // ReadWriter 接口既有方法也有类型,所以是一般接口
~string | ~[]rune
Read(p []byte) (n int, err error)
Write(p []byte) (n int, err error)
}
一般接口类型不能用来定义变量,只能用于泛型的类型约束中。所以以下的用法是错误的:
type Uint interface {
~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64
}
var uintInf Uint // 错误。Uint是一般接口,只能用于类型约束,不得用于变量定义
这一限制保证了一般接口的使用被限定在了泛型之中,不会影响到Go1.18之前的代码,同时也极大减少了书写代码时的心智负担。
泛型接口
所有类型的定义中都可以使用类型形参,所以接口定义自然也可以使用类型形参,观察下面这两个例子:
type DataProcessor[T any] interface {
Process(oriData T) (newData T)
Save(data T) error
}
type DataProcessor2[T any] interface {
int | ~struct{ Data interface{} }
Process(data T) (newData T)
Save(data T) error
}
因为引入了类型形参,所以这两个接口是泛型类型。而泛型类型要使用的话必须传入类型实参实例化才有意义。所以我们来尝试实例化一下这两个接口。因为 T
的类型约束是 any
,所以可以随便挑一个类型来当实参(比如 string
):
DataProcessor[string]
// 实例化之后的接口定义相当于如下所示:
type DataProcessor[string] interface {
Process(oriData string) (newData string)
Save(data string) error
}
经过实例化之后就好理解了,DataProcessor[string]
因为只有方法,所以它实际上就是个 基本接口(Basic interface),这个接口包含两个能处理 string 类型的方法。像下面这样实现了这两个能处理 string 类型的方法就算实现了这个接口:
type CSVProcessor struct {
}
// 注意,方法中 oriData 等的类型是 string
func (c CSVProcessor) Process(oriData string) (newData string) {
....
}
func (c CSVProcessor) Save(oriData string) error {
...
}
// CSVProcessor实现了接口 DataProcessor[string] ,所以可赋值
var processor DataProcessor[string] = CSVProcessor{}
processor.Process("name,age\nbob,12\njack,30")
processor.Save("name,age\nbob,13\njack,31")
// 错误。CSVProcessor没有实现接口 DataProcessor[int]
var processor2 DataProcessor[int] = CSVProcessor{}
再用同样的方法实例化 DataProcessor2[T]
:
DataProcessor2[string]
// 实例化后的接口定义可视为
type DataProcessor2[T string] interface {
int | ~struct{ Data interface{} }
Process(data string) (newData string)
Save(data string) error
}
DataProcessor2[string]
因为带有类型并集所以它是 一般接口(General interface),所以实例化之后的这个接口代表的意思是:
-
只有实现了
Process(string) string
和Save(string) error
这两个方法,并且以int
或struct{ Data interface{} }
为底层类型的类型才算实现了这个接口 -
一般接口(General interface) 不能用于变量定义只能用于类型约束,所以接口
DataProcessor2[string]
只是定义了一个用于类型约束的类型集
// XMLProcessor 虽然实现了接口 DataProcessor2[string] 的两个方法,但是因为它的底层类型是 []byte,所以依旧是未实现 DataProcessor2[string]
type XMLProcessor []byte
func (c XMLProcessor) Process(oriData string) (newData string) {
}
func (c XMLProcessor) Save(oriData string) error {
}
// JsonProcessor 实现了接口 DataProcessor2[string] 的两个方法,同时底层类型是 struct{ Data interface{} }。所以实现了接口 DataProcessor2[string]
type JsonProcessor struct {
Data interface{}
}
func (c JsonProcessor) Process(oriData string) (newData string) {
}
func (c JsonProcessor) Save(oriData string) error {
}
// 错误。DataProcessor2[string]是一般接口不能用于创建变量
var processor DataProcessor2[string]
// 正确,实例化之后的 DataProcessor2[string] 可用于泛型的类型约束
type ProcessorList[T DataProcessor2[string]] []T
// 正确,接口可以并入其他接口
type StringProcessor interface {
DataProcessor2[string]
PrintString()
}
// 错误,带方法的一般接口不能作为类型并集的成员(参考6.5 接口定义的种种限制规则
type StringProcessor interface {
DataProcessor2[string] | DataProcessor2[[]byte]
PrintString()
}
接口定义的种种限制规则
Go1.18 从开始,在定义类型集(接口)的时候增加了非常多十分琐碎的限制规则,其中很多规则都在之前的内容中介绍过了,但剩下还有一些规则因为找不到好的地方介绍,所以在这里统一介绍下:
-
用
|
连接多个类型的时候,类型之间不能有相交的部分(即必须是不交集):type MyInt int // 错误,MyInt的底层类型是int,和 ~int 有相交的部分 type _ interface { ~int | MyInt }
但是相交的类型中是接口的话,则不受这一限制:
type MyInt int type _ interface { ~int | interface{ MyInt } // 正确 } type _ interface { interface{ ~int } | MyInt // 也正确 } type _ interface { interface{ ~int } | interface{ MyInt } // 也正确 }
-
类型的并集中不能有类型形参
type MyInf[T ~int | ~string] interface { ~float32 | T // 错误。T是类型形参 } type MyInf2[T ~int | ~string] interface { T // 错误 }
-
接口不能直接或间接地并入自己
type Bad interface { Bad // 错误,接口不能直接并入自己 } type Bad2 interface { Bad1 } type Bad1 interface { Bad2 // 错误,接口Bad1通过Bad2间接并入了自己 } type Bad3 interface { ~int | ~string | Bad3 // 错误,通过类型的并集并入了自己 }
-
接口的并集成员个数大于一的时候不能直接或间接并入 comparable 接口
type OK interface { comparable // 正确。只有一个类型的时候可以使用 comparable } type Bad1 interface { []int | comparable // 错误,类型并集不能直接并入 comparable 接口 } type CmpInf interface { comparable } type Bad2 interface { chan int | CmpInf // 错误,类型并集通过 CmpInf 间接并入了comparable } type Bad3 interface { chan int | interface{comparable} // 理所当然,这样也是不行的 }
-
带方法的接口(无论是基本接口还是一般接口),都不能写入接口的并集中:
type _ interface { ~int | ~string | error // 错误,error是带方法的接口(一般接口) 不能写入并集中 } type DataProcessor[T any] interface { ~string | ~[]byte Process(data T) (newData T) Save(data T) error } // 错误,实例化之后的 DataProcessor[string] 是带方法的一般接口,不能写入类型并集 type _ interface { ~int | ~string | DataProcessor[string] } type Bad[T any] interface { ~int | ~string | DataProcessor[T] // 也不行 }