前一篇文章中我们简单介绍了约束条件的概念,知道了约束条件是通过接口来定义的。说到接口,大家都应该很熟悉了,它包含一组方法的声明,一个自定义类型只要实现了这组方法,就算实现了该接口。空接口interface{}
没有声明任何方法,所以任意类型都实现了空接口。前一篇文章中我们展示了,当一个接口被用作约束条件时,泛型函数中只能使用该接口声明的方法。这对于一些自定义类型是没有问题的,但是对于一些内置类型来讲就不够用了。
比如在下面的例子中,Sum
函数用来对一个int
型切片进行累加求和,我们想把它扩展成支持所有整型类型:
1 | func Sum(s []int) int { |
因为这些内置类型没有实现任何方法,所以我们无法用原有的接口来进行约束。直观来看,需要保证传入的类型参数支持加法运算符,然而同样的运算符作用于不同数据类型时也有不同的含义,比如加法运算符+
对于字符串类型是拼接操作,所以我们需要约束传入的类型参数为所有整型类型中的一种,因此在go1.18中,对接口进行了扩展以支持这种基于数据类型的约束条件,这就是我们接下来要介绍的类型集。
Type sets
还是从扩展之前的传统接口类型说起,接口声明了一组方法,我们称之为该接口的方法集。实际上,这组方法也同时定义了一个类型集,所有实现了这组方法的自定义类型都属于该类型集。因为自定义类型的数量是无穷的,所以传统接口定义的类型集是一个无限集。假如接口I1
声明了一个方法M1
,接口I2
声明了一个方法M2
,接口I3
声明了两个方法M1
和M2
,那么I3
所代表的类型集就是I1
和I2
的交集。同理,任何一个接口的类型集都是interface{}
的子集。
在go1.18中,接口被重新定义以完整支持constraints
。按照之前的定义,当一种数据类型实现了约束条件所对应的接口时,才算满足该约束。在新的定义中,只要给定的数据类型属于约束条件对应的类型集,就满足该约束。在语法层面,之前的接口定义允许接口中声明方法集,以及嵌入其他的接口。经过扩展之后,现在的接口中还允许列出任意的数据类型,如下面例子所示:
1 | type Int interface { |
这个Int
接口约束了传入的类型参数只能是int
类型,这种约束条件是合法的,但是没有什么意义,只限一种类型的话就称不上泛型了。所以,Go又允许我们使用|
运算符来组合多种数据类型,从而形成一个真正的类型集和,下面的示例代码给出了所有内置有符号整型构成的类型集:
1 | type Integer interface { |
有了这些还是不够,因为很多时候我们会基于现有的数据类型来定义新的类型,比如像JSON那样定义一种数值类型type Number float64
。使用泛型就是为了增强代码的复用性,我们不希望常常为了新定义的类型而去改动已有的约束。这个Number
类型的底层是float64,因此Go又提供了一个新的符号~
,让我们能够基于底层类型来定义约束条件。比如,下面代码所定义的类型集,包含float32
和float64
,以及所有底层是float32
和float64
的自定义类型:
1 | type Float interface { |
我们可以在接口中同时列出数据类型和声明方法集,以及嵌入其他的接口。但是需要注意的是,这种经过扩展的接口只能用作约束条件,不能像之前那样用来声明变量。如果要通过接口来使用Go的动态语言特性的话,还是要使用原始的接口语法,其中只能定义方法集、以及嵌入其他未被扩展的接口。
comparable
现在可以回过头来看看本文最初的那个Sum
函数了,在go1.18beta2标准库的constraints
包中(这个包在几天前被从标准库中移出,现在被移到了golang.org/x/exp这个mod中),已经为我们定义了一些常用的约束条件,其中整型被分开定义成了有符号整型和无符号整型两个约束,如下所示:
1 | type Signed interface { |
所以我们可以直接使用这些定义好的约束条件来重写我们的Sum
函数,重写后的代码如下所示:
1 | func Sum[T Signed | Unsigned](s []T) T { |
因为所有的整型类型都支持加法运算,所以我们使用扩展过的接口定义了类型集,作为约束条件进而实现了泛型的Sum
函数。但是Go的有些运算符是没有办法定义与之对应的类型集的,这个例外就是用来做相等性比较的两个运算符==
和!=
。除了一众基础类型之外,它们还可以用来比较结构体类型、数组类型和接口类型,只要结构体和数组的元素类型、或接口背后的具体类型支持相等性比较。我们没有办法定义包含结构体、数组和接口的类型集,何况还要考虑可比较性,专门针对这种情况,Go引入了一个预定义的约束条件comparable
。下述代码用来在一个切片中查找给定值的索引:
1 | func Index[T comparable](s []T, x T) int { |
好了,关于约束条件的介绍就到这里。有兴趣的同学可以阅读The Type Parameters Proposal,我们下一篇再见。