Go1.17探索与泛型尝鲜
Go 团队每年发布两次大版本,一般是在二月份和八月份发布。今天中午,Go1.17也是如期而至,带来各种优化和新功能,但是似乎泛型还没支持,但泛型的实现和测试已经包含在代码中,只是没有默认开启对泛型的支持,这也是为 Go1.18 版本泛型正式实装做铺垫。这样意味着最早在 2022年2月,我们就可以正式使用泛型进行代码开发了。
2021年8月21日,Go泛型在gotip中已经默认启用
泛型是什么, 有了它对我们有什么影响
在没有泛型时,如果想要实现类型转换,可以如何实现?那代码结构类似如下:
interface{}
比如说使用
interface{}
作为变量类型参数,在内部通过类型判断进入对应的处理逻辑
1 | ... |
Duck typing
将类型转化为特定表现的鸭子类型,通过接口定义的方法实现逻辑整合
1 | ... |
引入泛型的好处
- 通过泛型,可以针对多种类型编写一次代码,大大节省了编码时间。充分利用编译器的编译检查,保证程序的可靠性和鲁棒性。
- 借助泛型,可以减少代码的重复度,避免一处出现问题需要修改多处地方的尴尬情况。
Go 泛型发展
Go 官方团队泛型提案文档 Type Parameters in Go 和 Proposal: Go should have generics以及相关 issue。
Go 泛型尝鲜
我们尝试对上文中的类型转换使用泛型的方式重新编写一个小例子。
从简单的例子开始
Go 是强类型语言, 所以编写程序时传入变量需要指定 int
或 string
类型, 大致的函数如下:
1 | func PrintString(s string) {} |
试想下,如果对于不同类型的参数,需要为每个变量类型编写一个函数去实现,当然,我们可以使用
interface{}
来实现,但这样有很多的局限性。
首先定义一个泛型函数 PrintAnything
,它允许接收任何类型的参数(定义为 T
),函数定义如下:
1 | func PrintAnything[T any](thing T) {} |
any
可以表示接收任意类型的入参,此时要实现上文中函数可以这样调用实现相同的效果:
1 | PrintAnything("Hello! Go generics") |
如果这时我们使用简单的go run
命令运行,会发现提示语法错误:
1 | ❯❯ Downloads 12:26 go version |
Go1.17正式版本发布时,泛型代码已经内置,但默认未开启编译支持,我们可以通过支持泛型的在线Playground版本中调试,或者下载beta版本的go2go tool进行调试
在 Go1.17版本中,我们也可以通过gcflags
方式进行尝鲜
1 | go run -gcflags=-G=3 ~/main.go |
约束
从上面的例子中我们好像很容易的利用泛型实现了一个类似 fmt
包中打印变量的功能。接下来我们继续尝试实现 strings.Join
,让它接受一个任意类型的切片,并返回连接之后的字符串。
1 | func Join[T any](things []T) (result string) { |
如果这时我们使用简单的go run
命令运行,会发现提示语法错误:
1 | output := Join([]string{"a", "b", "c"}) |
Join
函数需要将任意类型转换为string
类型,需要检查v
是否有String()
方法,我们需要将代码改造下,让T
是一个约束类型,并实现String()
方法:
1 | type Stringer interface { |
所以我们的Join
函数现在的定义为:
1 | func Join[T Stringer] ... |
由于Stringer
保证 T 类型的任何值都有一个String()
方法。但是,如果你尝试调用Join()
某些类型不能满足Stringer
(例如int
):
1 | result := Join([]int{1, 2, 3}) |
比较运算符约束
例如我们需要实现一个比较两个参数是否相等的函数Equal
,它接受两个T
类型的参数,相等返回true
否则返回false
,代码如下:
1 | func Equal[T any](a, b T) bool { |
这里和上文是同一种问题,因为我们在Join()
用String()
的方法调用,但这里我们不能使用基于方法集的约束。我们需要将 T
限制为仅使用==
或!=
运算符的类型,这些类型称为可比较类型。使用内置comparable
类型约束,而不是any
.
1 | func Equal[T comparable] ... |
constraint 包
接下来,我们继续利用泛型做一些事情,比如有一个切片,我们需要取出最高值的元素,代码可能如下:
1 | func Max[T any](input []T) (max T) { |
尝试运行,不出意外是肯定报错了
1 | fmt.Println(Max([]int{1, 2, 3})) |
从报错来看,T
类型必须是可以有序可排列的。
1 | type Ordered interface { |
或者我们可以使用内置的constraint包
1 | func Max[T constraint.Ordered] ... |
泛型类型
我们已经利用类型实现了可以接受任何类型参数的函数。但是,如果我们想要建立一个类型,可以包含任何类型的?例如,
- “任何类型的切片”类型:对于任何给定的类型 T,Bunch[T]是值为
1
type Bunch[T any] []T
T
的切片。例如, - Bunch[int]是值为
int
的一个切片1
x := Bunch[int]{1, 2, 3}
- 采用泛型类型的泛型函数
1
func PrintBunch[T any](b Bunch[T]) {}
- 为泛型添加方法
1
func (b Bunch[T]) Print() {}
- 对泛型类型应用约束
1
type StringableBunch[T Stringer] []T