learn-tech/专栏/TonyBai·Go语言第一课/39驯服泛型:了解类型参数.md
2024-10-16 06:37:41 +08:00

697 lines
30 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

因收到Google相关通知网站将会择期关闭。相关通知内容
39 驯服泛型:了解类型参数
你好我是Tony Bai。
在专栏的结束语中我曾承诺要补充“泛型篇”帮助你入门Go泛型语法。在经历了2022年3月Go 1.18版本的泛型落地以及8月份Go 1.19对泛型问题的一轮修复后我认为是时候开讲Go泛型篇了。
虽说目前的Go泛型实现和最后一版的泛型设计方案相比还有差距依旧不是完全版还有一些特性没有加入还有问题亟待解决但对于入门Go泛型语法来说我认为已经是足够了。
不过在正式开讲之前我还有一些友情提示和支持泛型的主流编程语言之间的泛型设计与实现存在差异一样Go的泛型与其他主流编程语言的泛型也是不同的。我希望你在学习之前先看一下Go泛型设计方案已经明确不支持的若干特性比如
不支持泛型特化specialization即不支持编写一个泛型函数针对某个具体类型的特殊版本
不支持元编程metaprogramming即不支持编写在编译时执行的代码来生成在运行时执行的代码
不支持操作符方法operator method即只能用普通的方法method操作类型实例比如getIndex(k)而不能将操作符视为方法并自定义其实现比如一个容器类型的下标访问c[k]
不支持变长的类型参数type parameters
… …
这些特性如今不支持后续大概率也不会支持。所以小伙伴们尤其是来自Java、C++等语言阵营的小伙伴在进入Go泛型语法学习之前你一定要先了解Go团队的这些设计决策。
泛型篇的内容共有三讲我们将从泛型的基本语法也就是类型参数type parameter开启驯服泛型之旅接下来再搞定泛型的难点定义约束constraints最后我们再来谈谈Go泛型的使用时机。如果你还想Go泛型的演化简史请移步《加餐聊聊最近大热的Go泛型》这里我也做了详细分析。
那么今天这泛型篇的第一讲我们就来聚焦Go泛型的基本语法类型参数。下面我们通过一个最常见的泛型应用场景来开启今天的学习之旅。一个小提醒需要你注意泛型篇这三讲的所有示例代码均基于Go 1.19.1版本。
例子:返回切片中值最大的元素
正如小标题写的那样我们这个例子要实现一个函数该函数接受一个切片作为输入参数然后返回该切片中值最大的那个元素。题目并没有明确使用什么元素类型的切片我们就先以最常见的整型切片为例实现一个maxInt函数
// max_int.go
func maxInt(sl []int) int {
if len(sl) == 0 {
panic("slice is empty")
}
max := sl[0]
for _, v := range sl[1:] {
if v > max {
max = v
}
}
return max
}
func main() {
fmt.Println(maxInt([]int{1, 2, -4, -6, 7, 0})) // 输出7
}
maxInt的逻辑十分简单。我们使用第一个元素值(max := sl[0])作为max变量初值然后与切片后面的元素(sl[1:])进行逐一比较如果后面的元素大于max则将其值赋给max这样到切片遍历结束我们就得到了这个切片中值最大的那个元素即变量max
我们现在给它加一个新需求能否针对元素为string类型的切片返回其最大按字典序的元素值呢
答案肯定是能我们来实现这个maxString函数
// max_string.go
func maxString(sl []string) string {
if len(sl) == 0 {
panic("slice is empty")
}
max := sl[0]
for _, v := range sl[1:] {
if v > max {
max = v
}
}
return max
}
func main() {
fmt.Println(maxString([]string{"11", "22", "44", "66", "77", "10"})) // 输出77
}
maxString实现了返回string切片中值最大元素的需求。不过从实现上来看maxString与maxInt异曲同工只是切片元素类型不同罢了。这时如果让你参考上述maxInt或maxString实现一个返回浮点类型切片中最大值的函数maxFloat你肯定“秒秒钟”就可以给出一个正确的实现
// max_float.go
func maxFloat(sl []float64) float64 {
if len(sl) == 0 {
panic("slice is empty")
}
max := sl[0]
for _, v := range sl[1:] {
if v > max {
max = v
}
}
return max
}
func main() {
fmt.Println(maxFloat([]float64{1.01, 2.02, 3.03, 5.05, 7.07, 0.01})) // 输出7.07
}
问题来了!有代码洁癖的同学肯定已经嗅到了上面三个函数散发的“糟糕味道”:代码重复。上面三个函数除了切片的元素类型不同,其他逻辑都一样。
那么能否实现一个“通用”的函数可以处理上面三种元素类型的切片呢提到“通用”你一定想到了Go语言提供的anyinterface{}的别名),我们来试试:
// max_any.go
func maxAny(sl []any) any {
if len(sl) == 0 {
panic("slice is empty")
}
max := sl[0]
for _, v := range sl[1:] {
switch v.(type) {
case int:
if v.(int) > max.(int) {
max = v
}
case string:
if v.(string) > max.(string) {
max = v
}
case float64:
if v.(float64) > max.(float64) {
max = v
}
}
}
return max
}
func main() {
i := maxAny([]any{1, 2, -4, -6, 7, 0})
m := i.(int)
fmt.Println(m) // 输出7
fmt.Println(maxAny([]any{"11", "22", "44", "66", "77", "10"})) // 输出77
fmt.Println(maxAny([]any{1.01, 2.02, 3.03, 5.05, 7.07, 0.01})) // 输出7.07
}
我们看到maxAny利用any、type switch和类型断言type assertion实现了我们预期的目标。不过这个实现并不理想它至少有如下几个问题
若要支持其他元素类型的切片,我们需对该函数进行修改;
maxAny的返回值类型为anyinterface{}),要得到其实际类型的值还需要通过类型断言转换;
使用anyinterface{}作为输入参数的元素类型和返回值的类型由于存在装箱和拆箱操作其性能与maxInt等比起来要逊色不少实测数据如下
// max_test.go
func BenchmarkMaxInt(b *testing.B) {
sl := []int{1, 2, 3, 4, 7, 8, 9, 0}
for i := 0; i < b.N; i++ {
maxInt(sl)
}
}
func BenchmarkMaxAny(b *testing.B) {
sl := []any{1, 2, 3, 4, 7, 8, 9, 0}
for i := 0; i < b.N; i++ {
maxAny(sl)
}
}
$go test -v -bench . ./max_test.go max_any.go max_int.go
goos: darwin
goarch: amd64
... ...
BenchmarkMaxInt
BenchmarkMaxInt-8 398996863 2.982 ns/op
BenchmarkMaxAny
BenchmarkMaxAny-8 85883875 13.91 ns/op
PASS
ok command-line-arguments 2.710s
我们看到基于any(interface{})实现的maxAny其执行性能要比像maxInt这样的函数慢上数倍
在Go 1.18版本之前Go的确没有比较理想的解决类似上述通用问题的手段直到Go 1.18版本泛型落地后我们可以用泛型语法实现maxGenerics函数
// max_generics.go
type ordered interface {
~int | ~int8 | ~int16 | ~int32 | ~int64 |
~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 | ~uintptr |
~float32 | ~float64 |
~string
}
func maxGenerics[T ordered](sl []T) T {
if len(sl) == 0 {
panic("slice is empty")
}
max := sl[0]
for _, v := range sl[1:] {
if v > max {
max = v
}
}
return max
}
type myString string
func main() {
var m int = maxGenerics([]int{1, 2, -4, -6, 7, 0})
fmt.Println(m) // 输出7
fmt.Println(maxGenerics([]string{"11", "22", "44", "66", "77", "10"})) // 输出77
fmt.Println(maxGenerics([]float64{1.01, 2.02, 3.03, 5.05, 7.07, 0.01})) // 输出7.07
fmt.Println(maxGenerics([]int8{1, 2, -4, -6, 7, 0})) // 输出7
fmt.Println(maxGenerics([]myString{"11", "22", "44", "66", "77", "10"})) // 输出77
}
我们看到从功能角度看泛型版本的maxGenerics实现了预期的特性对于ordered接口中声明的那些原生类型以及以这些原生类型为底层类型underlying type的类型比如示例中的myStringmaxGenerics都可以无缝支持。并且maxGenerics返回的类型与传入的切片的元素类型一致调用者也无需通过类型断言做转换。
此外通过下面的性能基准测试我们也可以看出与maxAny相比泛型版本的maxGenerics性能要好很多但与原生版函数如maxInt等还有差距。关于泛型的运行时性能损耗问题我们在泛型篇第三讲中会有说明。性能测试如下
$go test -v -bench . ./max_test.go max_any.go max_int.go max_generics.go
goos: darwin
goarch: amd64
BenchmarkMaxInt
BenchmarkMaxInt-8 400910706 2.983 ns/op
BenchmarkMaxAny
BenchmarkMaxAny-8 85257433 14.04 ns/op
BenchmarkMaxGenerics
BenchmarkMaxGenerics-8 209468593 5.701 ns/op
PASS
ok command-line-arguments 4.492s
通过这个例子我们也可以看到Go泛型十分适合实现一些操作容器类型比如切片、map等的算法这也是Go官方推荐的第一种泛型应用场景此类容器算法的泛型实现使得容器算法与容器内元素类型彻底解耦
不过看到这里很多同学可能会抱怨看不懂maxGenerics的语法别急接下来我们就来基于这个例子进行泛型基本语法的学习。
类型参数type parameters
根据官方说法由于泛型generic一词在Go社区中被广泛使用所以官方也就接纳了这一说法。但Go泛型方案的实质是对类型参数type parameter的支持包括
泛型函数generic function带有类型参数的函数
泛型类型generic type带有类型参数的自定义类型
泛型方法generic method泛型类型的方法。
下面我们先以泛型函数为例来具体说明一下什么是类型参数。
泛型函数
我们回顾一下上面的示例maxGenerics就是一个泛型函数我们看一下maxGenerics的函数原型
func maxGenerics[T ordered](sl []T) T {
// ... ...
}
我们看到maxGenerics这个函数与我们之前学过的普通Go函数ordinary function相比至少有两点不同
maxGenerics函数在函数名称与函数参数列表之间多了一段由方括号括起的代码[T ordered]
maxGenerics参数列表中的参数类型以及返回值列表中的返回值类型都是T而不是某个具体的类型。
maxGenerics函数原型中多出的这段代码[T ordered]就是Go泛型的类型参数列表type parameters list示例中这个列表中仅有一个类型参数Tordered为类型参数的类型约束type constraint。类型约束之于类型参数就好比常规参数列表中的类型之于常规参数。关于类型约束我们还会在下一讲中详细说明。
Go语言规范规定函数的类型参数列表位于函数名与函数参数列表之间由方括号括起的固定个数的、由逗号分隔的类型参数声明组成其一般形式如下
func genericsFunc[T1 constraint1, T2, constraint2, ..., Tn constraintN](ordinary parameters list) (return values list)
函数一旦拥有类型参数就可以用该参数作为常规参数列表和返回值列表中修饰参数和返回值的类型。我们继续maxGenerics泛型函数为例分析它拥有一个类型参数T在常规参数列表中T被用作切片的元素类型在返回值列表中T被用作返回值的类型。
按Go惯例类型参数名的首字母通常采用大写形式并且类型参数必须是具名的即便你在后续的函数参数列表、返回值列表和函数体中没有使用该类型参数也是这样。比如下面例子中的类型参数T
func print[T any]() { // 正确
}
func print[any]() { // 编译错误all type parameters must be named
}
和常规参数列表中的参数名唯一一样在同一个类型参数列表中类型参数名字也要唯一下面这样的代码将会导致Go编译器报错
func print[T1 any, T1 comparable](sl []T) { // 编译错误T1 redeclared in this block
//...
}
常规参数列表中的参数有其特定作用域,即从参数声明处开始到函数体结束。和常规参数类似,泛型函数中类型参数也有其作用域范围,这个范围从类型参数列表左侧的方括号[开始,一直持续到函数体结束,如下图所示:
类型参数的作用域也决定了类型参数的声明顺序并不重要,也不会影响泛型函数的行为,于是下面的泛型函数声明与上图中的函数是等价的:
func foo[M map[E]T, T any, E comparable](m M)(E, T) {
//... ...
}
到这里,泛型函数的结构我们已经了解完了,接下来我们来看一下如何调用泛型函数。
调用泛型函数
在前面的讲解中,我一直使用“类型参数”这个名称。但在学习调用泛型函数之前,我们需要对“类型参数”做一下细分。
和普通函数有形式参数与实际参数一样类型参数也有类型形参type parameter和类型实参type argument之分。其中类型形参就是泛型函数声明中的类型参数以前面示例中的maxGenerics泛型函数为例如下面代码maxGenerics的类型形参就是T而类型实参则是在调用maxGenerics时实际传递的类型int
// 泛型函数声明T为类型形参
func maxGenerics[T ordered](sl []T) T
// 调用泛型函数int为类型实参
m := maxGenerics[int]([]int{1, 2, -4, -6, 7, 0})
从上面这段代码我们也可以看出调用泛型函数与调用普通函数的区别。在调用泛型函数时除了要传递普通参数列表对应的实参之外还要显式传递类型实参比如这里的int。并且显式传递的类型实参要放在函数名和普通参数列表前的方括号中。
在反复揣摩上面代码和说明后,你可能会提出这样的一个问题:如果泛型函数的类型形参较多,那么逐一显式传入类型实参会让泛型函数的调用显得十分冗长,比如:
foo[int, string, uint32, float64](1, "hello", 17, 3.14)
这样的写法对开发者而言显然谈不上十分友好。其实不光大家想到了这个问题Go团队的泛型实现者们也考虑了这个问题并给出了解决方法函数类型实参的自动推断function argument type inference
顾名思义这个机制就是通过判断传递的函数实参的类型来推断出类型实参的类型从而允许开发者不必显式提供类型实参下面是以maxGenerics函数为例的类型实参推断过程示意图
我们看到当maxGenerics函数传入的实际参数为[]int{…}时Go编译器会将其类型[]int与泛型函数参数列表中对应参数的类型[]T作比较并推断出T == int这一结果。当然这个例子的推断过程较为简单那些有难度的甚至无法肉眼可见的就交给Go编译器去处理吧我们没有必要过于深入。
不过,这个类型实参自动推断有一个前提,你一定要记牢,那就是它必须是函数的参数列表中使用了的类型形参,否则就会像下面的示例中的代码,编译器将报无法推断类型实参的错误:
func foo[T comparable, E any](a int, s E) {
}
foo(5, "hello") // 编译器错误cannot infer T
在编译器无法推断出结果时我们可以给予编译器“部分提示”比如既然编译器无法推断出T的实参类型那我们就显式告诉编译器T的实参类型即在泛型函数调用时在类型实参列表中显式传入T的实参类型但E的实参类型依然由编译器自动推断示例代码如下
var s = "hello"
foo[int](5, s) //ok
foo[int,](5, s) //ok
那么,除了函数参数列表中的参数类型可以作为类型实参推断的依据外,函数返回值的类型是否也可以呢?我们看下面示例:
func foo[T any](a int) T {
var zero T
return zero
}
var a int = foo(5) // 编译器错误cannot infer T
println(a)
我们看到这个函数仅在返回值中使用了类型参数但编译器没能推断出T的类型所以我们切记不能通过返回值类型来推断类型实参。
有了函数类型实参推断后,在大多数情况下,我们调用泛型函数就无须显式传递类型实参了,开发者也因此获得了与普通函数调用几乎一致的体验。
其实泛型函数调用是一个不同于普通函数调用的过程,为了揭开其中的“奥秘”,接下来我们就把镜头放慢,看看泛型函数调用过程究竟发生了什么。
泛型函数实例化instantiation
我们还以maxGenerics为例来演示一下这个过程
maxGenerics([]int{1, 2, -4, -6, 7, 0})
上面代码是对maxGenerics泛型函数的一次调用Go对这段泛型函数调用代码的处理分为两个阶段如下图所示
我们看到Go首先会对泛型函数进行实例化instantiation即根据自动推断出的类型实参生成一个新函数当然这一过程是在编译阶段完成的不会对运行时性能产生影响然后才会调用这个新函数对输入的函数参数进行处理。
我们也可以用一种更形象的方式来描述上述泛型函数的实例化过程。实例化就好比一家生产“求最大值”机器的工厂,它会根据要比较大小的对象的类型将这样的机器生产出来。以上面的例子来说,整个实例化过程如下:
工厂接单调用maxGenerics([]int{…})工厂师傅发现要比较大小的对象类型为int
模具检查与匹配检查int类型是否满足模具的约束要求即int是否满足ordered约束如满足则将其作为类型实参替换maxGenerics函数中的类型形参T结果为maxGenerics[int]
生产机器将泛型函数maxGenerics实例化为一个新函数这里将其起名为maxGenericsInt其函数原型为func([]int)int。本质上maxGenericsInt := maxGenerics[int]。
我们实际的Go代码也可以真实得到这台新生产出的“机器”如下面代码所示
maxGenericsInt := maxGenerics[int] // 实例化后得到的新“机器”maxGenericsInt
fmt.Printf("%T\n", maxGenericsInt) // func([]int) int
一旦针对int对象的“求最大值”的机器被生产出来了它就可以对目标对象进行处理了这和普通的函数调用没有区别。这里就相当于调用如下代码
maxGenericsInt([]int{1, 2, -4, -6, 7, 0}) // 输出7
整个过程只需检查传入的函数实参([]int{1, 2, …}的类型与maxGenericsInt函数原型中的形参类型[]int是否匹配即可。
另外要注意当我们使用相同类型实参对泛型函数进行多次调用时Go仅会做一次实例化并复用实例化后的函数比如
maxGenerics([]int{1, 2, -4, -6, 7, 0})
maxGenerics([]int{11, 12, 14, -36,27, 0}) // 复用第一次调用后生成的原型为func([]int) int的函数
好了关于泛型函数的讲解就先告一段落接下来我们再来看Go对类型参数的另一类支持带有类型参数的自定义类型即泛型类型。
泛型类型
所谓泛型类型就是在类型声明中带有类型参数的Go类型比如下面代码中的maxableSlice
// maxable_slice.go
type maxableSlice[T ordered] struct {
elems []T
}
顾名思义maxableSlice是一个自定义切片类型这个类型的特点是总可以获取其内部元素的最大值其唯一的要求是其内部元素是可排序的它通过带有ordered约束的类型参数来明确这一要求。像这样在定义中带有类型参数的类型就被称为泛型类型generic type
从例子中的maxableSlice类型声明中我们可以看到在泛型类型中类型参数列表放在类型名字后面的方括号中。和泛型函数一样泛型类型可以有多个类型参数类型参数名通常是首字母大写的这些类型参数也必须是具名的且命名唯一。其一般形式如下
type TypeName[T1 constraint1, T2 constraint2, ..., Tn constraintN] TypeLiteral
和泛型函数中类型参数有其作用域一样,泛型类型中类型参数的作用域范围也是从类型参数列表左侧的方括号[开始,一直持续到类型定义结束的位置,如下图所示:
这样的作用域将方便我们在各个字段中灵活使用类型参数,下面是一些自定义泛型类型的示例:
type Set[T comparable] map[T]struct{}
type sliceFn[T any] struct {
s []T
cmp func(T, T) bool
}
type Map[K, V any] struct {
root *node[K, V]
compare func(K, K) int
}
type element[T any] struct {
next *element[T]
val T
}
type Numeric interface {
~int | ~int8 | ~int16 | ~int32 | ~int64 |
~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 | ~uintptr |
~float32 | ~float64 |
~complex64 | ~complex128
}
type NumericAbs[T Numeric] interface {
Abs() T
}
我们看到泛型类型中的类型参数可以用来作为类型声明中字段的类型比如上面的element类型、复合类型的元素类型比如上面的Set和Map类型或方法的参数和返回值类型如NumericAbs接口类型等。
如果要在泛型类型声明的内部引用该类型名必须要带上类型参数如上面的element结构体中的next字段的类型*element[T]。按照泛型设计方案,如果泛型类型有不止一个类型参数,那么在其声明内部引用该类型名时,不仅要带上所有类型参数,类型参数的顺序也要与声明中类型参数列表中的顺序一致,比如:
type P[T1, T2 any] struct {
F *P[T1, T2] // ok
}
不过从实测结果来看Go 1.19版本对于下面不符合技术方案的泛型类型声明也并未报错:
type P[T1, T2 any] struct {
F *P[T2, T1] // 不符合技术方案但Go 1.19编译器并未报错
}
了解了如何声明一个泛型类型后,我们再来看看如何使用这些泛型类型。
使用泛型类型
和泛型函数一样使用泛型类型时也会有一个实例化instantiation过程比如
var sl = maxableSlice[int]{
elems: []int{1, 2, -4, -6, 7, 0},
}
Go会根据传入的类型实参int生成一个新的类型并创建该类型的变量实例sl的类型等价于下面代码
type maxableIntSlice struct {
elems []int
}
看到这里你可能会问泛型类型是否可以像泛型函数那样实现类型实参的自动推断呢很遗憾目前的Go 1.19尚不支持下面代码会遭到Go编译器的报错
var sl = maxableSlice {
elems: []int{1, 2, -4, -6, 7, 0}, // 编译器错误cannot use generic type maxableSlice[T ordered] without instantiation
}
不过这一特性在Go的未来版本中可能会得到支持。
既然涉及到了类型你肯定会想到诸如类型别名、类型嵌入等Go语言机制那么这些语言机制对泛型类型的支持情况又是如何呢我们逐一来看一下。
泛型类型与类型别名-
在专栏前面的讲解中我们学习过类型别名type alias。我们知道类型别名与其绑定的原类型是完全等价的但这仅限于原类型是一个直接类型即可直接用于声明变量的类型。那么将类型别名与泛型类型绑定是否可行呢我们来看一个示例
type foo[T1 any, T2 comparable] struct {
a T1
b T2
}
type fooAlias = foo // 编译器错误cannot use generic type foo[T1 any, T2 comparable] without instantiation
在上述代码中我们为泛型类型foo建立了类型别名fooAlias但编译这段代码时编译器还是报了错误
这是因为,泛型类型只是一个生产真实类型的“工厂”,它自身在未实例化之前是不能直接用于声明变量的,因此不符合类型别名机制的要求。泛型类型只有实例化后才能得到一个真实类型,例如下面的代码就是合法的:
type fooAlias = foo[int, string]
也就是说我们只能为泛型类型实例化后的类型创建类型别名实际上上述fooAlias等价于实例化后的类型fooInstantiation
type fooInstantiation struct {
a int
b string
}
泛型类型与类型嵌入-
类型嵌入是运用Go组合设计哲学的一个重要手段。引入泛型类型之后我们依然可以在泛型类型定义中嵌入普通类型比如下面示例中Lockable类型中嵌入的sync.Mutex
type Lockable[T any] struct {
t T
sync.Mutex
}
func (l *Lockable[T]) Get() T {
l.Lock()
defer l.Unlock()
return l.t
}
func (l *Lockable[T]) Set(v T) {
l.Lock()
defer l.Unlock()
l.t = v
}
在泛型类型定义中我们也可以将其他泛型类型实例化后的类型作为成员。现在我们改写一下上面的Lockable为其嵌入另外一个泛型类型实例化后的类型Slice[int]
type Slice[T any] []T
func (s Slice[T]) String() string {
if len(s) == 0 {
return ""
}
var result = fmt.Sprintf("%v", s[0])
for _, v := range s[1:] {
result = fmt.Sprintf("%v, %v", result, v)
}
return result
}
type Lockable[T any] struct {
t T
Slice[int]
sync.Mutex
}
func main() {
n := Lockable[string]{
t: "hello",
Slice: []int{1, 2, 3},
}
println(n.String()) // 输出1, 2, 3
}
我们看到代码使用泛型类型名Slice作为嵌入后的字段名并且Slice[int]的方法String被提升为Lockable实例化后的类型的方法了。同理在普通类型定义中我们也可以使用实例化后的泛型类型作为成员比如让上面的Slice[int]嵌入到一个普通类型Foo中示例代码如下
type Foo struct {
Slice[int]
}
func main() {
f := Foo{
Slice: []int{1, 2, 3},
}
println(f.String()) // 输出1, 2, 3
}
此外Go泛型设计方案支持在泛型类型定义中嵌入类型参数作为成员比如下面的泛型类型Lockable内嵌了一个类型T且T恰为其类型参数
type Lockable[T any] struct {
T
sync.Mutex
}
不过Go 1.19版本编译上述代码时会针对嵌入T的那一行报如下错误
编译器报错embedded field type cannot be a (pointer to a) type parameter
关于这个错误Go官方在其issue中给出了临时的结论暂不支持。
泛型方法
在专栏基础篇的学习中我们知道Go类型可以拥有自己的方法method泛型类型也不例外为泛型类型定义的方法称为泛型方法generic method接下来我们就来讲讲如何定义和使用泛型方法。
我们用一个示例给maxableSlice泛型类型定义max方法看一下泛型方法的结构
func (sl *maxableSlice[T]) max() T {
if len(sl.elems) == 0 {
panic("slice is empty")
}
max := sl.elems[0]
for _, v := range sl.elems[1:] {
if v > max {
max = v
}
}
return max
}
我们看到在定义泛型类型的方法时方法的receiver部分不仅要带上类型名称还需要带上完整的类型形参列表如maxableSlice[T]),这些类型形参后续可以用在方法的参数列表和返回值列表中。
不过在Go泛型目前的设计中泛型方法自身不可以再支持类型参数了不能像下面这样定义泛型方法
func (f *foo[T]) M1[E any](e E) T { // 编译器错误syntax error: method must have no type parameters
//... ...
}
关于泛型方法未来是否能支持类型参数目前Go团队倾向于否但最终结果Go团队还要根据Go社区在使用泛型过程中的反馈而定。
在泛型方法中receiver中某个类型参数如果没有在方法参数列表和返回值中使用可以用“_”代替但不能不写比如
type foo[A comparable, B any] struct{}
func (foo[A, B]) M1() { // ok
}
func (foo[_, _]) M1() { // ok
}
func (foo[A, _]) M1() { // ok
}
func (foo[]) M1() { // 错误receiver部分缺少类型参数
}
另外泛型方法中的receiver中类型参数名字可以与泛型类型中的类型形参名字不同位置和数量对上即可。我们还以上面的泛型类型foo为例可以为它添加下面方法
type foo[A comparable, B any] struct{}
func (foo[First, Second]) M1(a First, b Second) { // First对应类型参数ASecond对应类型参数B
}
小结
好了,今天的课讲到这里就结束了,现在我们一起来回顾一下吧。
在这一讲中我们一起学习了Go泛型的基本语法类型参数。类型参数是Go泛型方案的具体实现通过类型参数我们可以定义泛型函数、泛型类型以及对应的泛型方法。
泛型函数是带有类型参数的函数,在函数名称与参数列表之间声明的类型参数列表使得泛型函数的运行逻辑与参数/返回值类型解耦。调用泛型函数与普通函数略有不同泛型函数需要进行实例化后才能生成真正执行的、带有类型信息的函数。同时Go泛型支持的类型实参推断也使得开发者在大多数情况下无需显式传递类型实参获得与普通函数调用几乎一致的体验。
泛型类型是带有类型参数的类型泛型类型的类型参数放在类型名称后面的类型参数列表中声明类型参数后续可以在泛型类型声明中用作成员字段的类型或复合类型成员元素的类型。不过目前Go 1.19版本Go尚不支持泛型类型的类型实参的自动推断我们在泛型类型实例化时需要显式传入类型实参。
与泛型类型绑定的方法被称为泛型方法,泛型方法的参数列表和返回值列表中可以使用泛型类型的类型参数,但泛型方法目前尚不支持声明自己的类型参数列表。
Go泛型的引入使得Go开发人员在interface{}之后又拥有了一种编写“通用代码”的手段,并且这种新手段因其更多在编译阶段的检查而变得更加安全,也因其减少了运行时的额外开销使得代码性能更好。
思考题
使用过其他编程语言泛型语法特性的小伙伴们可能会问为什么Go在方括号“[]”中声明类型参数,而不是使用其他语言都用的尖括号“<>”呢?你可以思考一下。
欢迎在评论区写下你的想法,我们泛型篇的第二讲见。