面试官提问三个Go接口的概念, 10年gopher竟无言以对

自 Go 1.18后, Go的interface的含义有所变化, 三个新的和Go接口有关的概念很多人还不知道: type set(类型集合)、specific type(特定类型)和structural type(结构类型)。

type set (类型集合)

type set称之为类型集合,一些关注Go泛型的朋友其实也对此有些了解,它是Go 1.18新增加的一个概念。

Go 1.18之前,Go的接口代表了一组方法的集合(method set),凡是实现了这些方法集合的类型,都被称之为实现了这个接口。Go不像Java语言,需要显示地定义某个类实现某个接口,Go不需要这样,在Go中,只要一个类型实现了某个接口定义的所有方法,它就实现了这个接口,可以赋值给这个接口类型的变量,或者作为这个接口类型的方法的实参或者返回值,这种设计有时候也被叫做鸭子类型(duck typing)。只要它走起来像鸭子,叫起来像鸭子,那么它就是鸭子,这是一个很经典的对鸭子类型的描述。

在Go 1.18中,接口不再代表方法的集合了,而是代表类型的集合(type set)。只要某种类型在这个接口的类型集合中,那么我们就说这种类型实现了这个接口。如果这个接口被用作类型约束,那么在这个接口定义的类型集合中的任意一个元素,都可以实例化这个类型参数。

所以,实际上,为了支持接口作为类型约束的扩展,Go语言规范不得不重新定义接口的含义了,这也是类型集合出现的原因。

其实,接口的方法集的概念还在 一个接口的方法集是这个接口的类型集合中所有元素的方法集的交集

r本文假定你对Go泛型有了一定的了解。假定你还不了解,那么你必须知道,Go 1.18中接口除了原先的方法元素还,还支持包含类型元素,类型元素可以是某个类型T、或者是近似类型~T,或者是它们的联合int|int8|int16|int32|int64|~string

如果一个接口I嵌入了另外一个接口E,那么I的类型集是它显示定义的类型集合和嵌入的接口E的类型集合的交集。相当于E把接口I的类型集收窄了。

如何判断一个接口的类型集呢?请遵循下面的原则:

  • 空接口anyinterface{}的类型集是所有类型的集合
    所以像intstringstrcut{}MyStructfunc foobar()chan intmap[int]string[]int等等都在空接口的类型集合中

  • 非空接口的类型集合是接口元素的类型集合的交集
    那么什么是接口元素的类型集合呢?参照下面的四条。
    前面我们已经提到,接口元素包含类型元素和方法元素。

    • 一个方法的类型集合是定义这个方法的所有类型,也就是只要某个类型的方法集包含这个方法,那么它就属于这个方法的类型集合
      比如接口中有String() string这样一个方法,那么所有实现这个方法的类型都属于String() string定义的类型集合,比如net.IP

    • 一个非接口类型的类型集合就是只包含这个类型的类型集合
      比如int的类型集合只包含int这样一个元素。

    • 近似元素~T的类型集合是所有底层类型为T的所有类型的集合

    比如type MyInt int中的MyInt就属于~int的类型集合

    • 联合元素t1|t2|…|tn的类型集合是这些联合元素类型集合的并集

下面的例子列举了一些类型的集合:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// 这个集合的类型集合只有int这一种类型
interface {
int
}
// 这个接口代表所有底层为int类型的所有类型
interface {
~int
}
// 这个接口代表底层为int,并且实现了String方法的所有类型
interface {
~int
String() string
}
// 这个接口的类型集合是空集,因为不可能一个元素既是int又是string类型
interface {
int
string
}
// Floats代表所有底层是浮点数的类型 (底层为float32或者float64)
type Floats interface {
~float32 | ~float64
}

specific type (特定类型) 和 specific type set

接口另外一个很重要的概念就是specific type (特定类型)。

只有包含类型元素的接口才定义了特定类型(可能是空的类型)。

如果不严格的讲,特定类型是出现在类型元素中定义的那些类型T~Tt1|t2|...|tn中的t1t2...tn

更准确地说,对于给定的接口I,特定类型的集合对应于该接口代表的类型集合𝑅,这里要求𝑅是非空且有限的。否则,如果𝑅为空或无限,则接口没有特定类型。

对于一个给定的接口、类型元素或者类型,它代表的类型集合𝑅定义如下:

  • 对于一个没有任何类型元素的接口,它的𝑅是所有的元素(无限)。
    所以它没有特定类型。

  • 如果一个接口有类型元素,它的𝑅是它的元素代表的类型的交集
    至于有没有特定类型要看𝑅是否是非空且有限。

  • 对于一个非接口类型T,或者~T, 它的𝑅是包含类型T的集合

  • 对于联合元素t1|t2|…|tn, 它的𝑅是这些项代表类型的并集

下面是特定类型的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
type Celsius float32
type Kelvin float32
interface{} // 无限,所以没有特定类型
interface{ int } // 特定类型是int
interface{ ~string } // 特定类型是string
interface{ int|~string } // 特定类型是int, string
interface{ Celsius|Kelvin } // 特定类型是Celsius, Kelvin
interface{ float64|any } // 没有特定类型,因为联合类型的代表类型是无限的
interface{ int; m() } // 特定类型是int
interface{ ~int; m() } // 特定类型是int
interface{ int; any } // 特定类型是int,int和any的交集
interface{ int; string } // 没有特定类型

type set vs specific type set

类型集合和特定类型集合还是有区别的,从上面它们的定义可以看出来。

一个接口即使类型为空,它的特定类型集合可能不为空。
比如interface{ int; m() },它的类型集合是空的(int没有实现m方法),但是它的特定类型是int

一个接口即使有有限的特定类型,它的类型集合也可能是无限的
比如interface{ ~int; m() },它的特定类型是int,但是它的类型集合确是无限的(任何底层为int并且实现了方法m的类型都属于它的类型集合)

那么定义特定类型有什么用呢?

特定类型的应用

特定类型主要用于判断类型参数是否支持索引, 像a[x]这样的类型。

比如一个表达式a[x], a这个实例的类型可能是数组、指向数组的指针、slice、字符串、map。

如果a的类型是类型参数P的话,那么我们的代码a[x]在什么条件下才不会编译出错?

要求的条件就和特定类型有关了:

  • P必须有特定类型
  • 对于P的特定类型的值a,支持a[x]这种索引写法
  • P的所有特定类型必须相同。在这里,string类型的元素类型是byte (https://github.com/golang/go/issues/49551)
  • 如果P的特定类型包含map类型的话,那么它的所有特定类型必须是map,而且所有的key的类型是相同的
    所以有时候你定义了一个包含map、slice、string的联合元素接口的话,这个接口的实例你不能使用a[x]索引类型,元素的类型都是int
  • a[x]是数组、slice、string的索引为x的元素,或者是map类型key为x的元素,a[x]的类型必须相同
  • 如果P的特定类型包含string类型,那么不能给a[x]赋值(字符串是不可变的)

特定类型还用作类型转化定义上。
对于一个变量x,如果它的类型是V, 要转换成的类型是T, 只要满足下面一条,x就可以转换成T类型:

  • V的每一个特定类型的值都可以转换成T的每一个特定类型
  • 只有V是类型参数,T不是,那么V的每一个特定类型的值都可以转换成T
  • 只有T是类型参数,x可以转换成T的每一个特定类型

一句话,是类型参数就满足每一个特定类型,不是类型参数就满足这个类型。

另外,对于类型参数,要调用内建的函数lencap,必须要求它们的特定类型允许使用这些内建函数。

structural type (结构类型)

一个接口T要被成为结构化的(structural),需要满足下面的条件之一:

  1. 存在一个单一的类型U,它是T的类型集合中的每一个元素相同的底层类型
  2. T的类型集合只包含chan类型,并且它们的元素类型都是E, 所有的chan的方向包含相同的方向(不一定要求完全相同)

结构化类型包含一个结构类型,根据上面的条件不同,结构类型可能是:

  1. 类型U, 或者
  2. 如果T只包含双向chan的话,结构类型为chan E,否则可能是chan<- E或者<-chan E

下面是包含结构类型的结构化接口:

1
2
3
4
5
6
7
8
9
10
11
interface{ int } // 结构类型为 int
interface{ Celsius|Kelvin } // 结构类型为 float32
interface{ ~chan int } // 结构类型为 chan int
interface{ ~chan int|~chan<- int } // 结构类型为 chan<- int
interface{ ~[]*data; String() string } // 结构类型为 *data
// 下面的例子不包含结构类型,所以是非结构化接口
interface{} // 没有固定单一的底层类型
interface{ Celsius|float64 } // 底层类型不相同
interface{ chan int | chan<- string } // channel的元素类型不相同
interface{ <-chan int | chan<- int } // channel没有相同的方向

在Go语言规范中,并没有对结构化接口有更多的介绍,如何使用,更多是是它内部获取底层的结构类型,以及做类型检查,比如下面的例子就会报no structural type编译错误:

1
2
3
4
5
6
7
8
9
10
11
12
13
type myByte1 []byte
func _[T interface{ []byte | myByte1 | []int }] (x T, i, j, k int) {
var _ T = x[i:j:k] // 底层类型不一致
}
func _[T interface{ []byte | myByte1 | []int | string }] (x T, i, j, k int) {
var _ T = x[i:j]
}
// 下面这个函数没问题,因为string的底层页被看做[]byte
func _[T interface{ []byte | myByte1 | myByte2 | string }] (x T, i, j, k int) {
var _ T = x[i:j]
}

下面的代码也会报M has no structural type编译错误

1
2
3
4
5
6
7
8
9
10
type multiMapOfInt interface {
map[int]int | map[float64]int | map[string]int | map[complex64]int
}
func arraySummer[M multiMapOfInt](mp M) (sum int) {
for _, v := range mp {
sum += v
}
return
}

比如下面大家使用Go泛型的时候常会犯的错误,虽然[]bytemap[int]bytestring都能range,而且key(index)、value类型都一样,但是也会报R has no structural type错误:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package main
import "fmt"
type rangeType interface {
[]byte | map[int]byte | string
}
func rangeIt[R rangeType](r R) {
for i, v := range r {
fmt.Println(i, v)
}
}
func main() {
rangeIt(map[int]byte{1: 1, 2: 2, 3: 3})
}