Go的泛型已经讨论了多年了,具体热烈的讨论可以参考
https://github.com/golang/go/issues/43651
这个issue。
自从草案设计以来,在漫长的等待后,该提案终于在2020年3月发布了。
泛型应该在Go1.18
,也就是2022年2月可以用起来。
由于提案内容非常多,把它全部读完很费时间。我将尝试以更简洁的方式用示例介绍关键特性。
如何开始Go
泛型
Go
团队提供了两种方法,以便在官方发布之前尝试新功能。
-
https://gotipplay.golang.org/
一个在线的的运行Go代码平台 -
go2go tools
可以本地安装,具体安装方法
#下载源码
git clone https://go.googlesource.com/g
# checkout到go2go分支
cd go && git checkout dev.go2go
# 编译
cd src && ./make.bash
# 安装build tools
GO111MODULE=on go get golang.org/x/tools/gopls@dev.go2go golang.org/x/tools@dev.go2go
然后,你就可以通过创建.go2
的GO
文件来体验新功能!
理解Go
泛型
主要功能包括以下两个部分:
-
类型参数。这里的
Type
可以是函数的参数类型,也可以是结构中字段的类型。 -
约束。一般情况, 约束使用相同的行为
(func)
组合多个类型,使这些类型的任何实例对象都能运行相应的泛型方法。如果你觉得约束部分的解释有点抽象,下面这个例子可以帮助你更好地理解。
函数类型参数
看看官方给出的最简单的例子
import(
"fmt"
)
func Print(T any)(s []T) {
for _, v := range s {
fmt.Printf("%v ", v)
}
}
func main() {
PrintSlice(int)([]int{1, 2, 3, 4, 5})
}
T any
表示函数参数的类型。因此,该函数可以打印元素类型为T
的切片的所有元素。
因为可以直接打印所有Go类型,main
函数中声明的类型(int)
可以省略,Go编译器可以进行正确的类型推断。
所以这种写法也合理。
Print([]string{"Hello, ", "World\n"})
再写个更复杂的例子。
func Map[K, V any](s []K, transform func(K) V) []V {
rs := make([]V, len(s))
for i, v := range s {
rs[i] = transform(v)
}
return rs
}
func main() {
arr := []string{"a", "b"}
dArr := Map(arr, func(s string) string { return s + s})
fmt.Println(dArr);
}
// output: [aa bb]
结构体中类型参数
比如可以像下面这样写:
type Vector[T any] []T
type LinkedList[T any] struct {
next *LinkedList[T]
val T
}
type Pair[T1, T2 any] struct {
v1 T1
v2 T2
}
type Tuple[T1, T2, T3 any] struct {
v1 T1
v2 T2
v3 T3
}
然后,就可以从这些结构中抽象出通用的方法。比如找到最小的元素
。
func Min[T any](s []T) T {
r := s[0]
for _, v := range s[1:] {
if v < r {
r = v
}
}
return r
}
但是 上面的代码没办法编译通过,报错如下:
./prog.go:27:7: invalid operation: cannot compare v < r (operator < not defined on T)
报错的意思是: 在这里不能比较v和r,因为v
和r
是T
类型,而T
类型没有比较操作符
有很多解决方案。比如,可以抽象<
的比较操作,定义一个类似于Comparable
(在Java
中)的比较函数,
然后在调用时传入compare
函数。实现如下。
func Min[T any](s []T, compare func(T, T) int) T {
r := s[0]
for _, v := range s[1:] {
if compare(r, v) == 1 {
r = v
}
}
return r
}
func main() {
arr := []string{"a", "b"}
dArr := Map(arr, func(s string) string { return s + s })
fmt.Println(dArr)
v := make(Vector[int], 3)
v[0] = 1
v[1] = 2
v[2] = -1
fmt.Println(Smallest(v, func(i1 int, i2 int) int {
if i1 > i2 {
return 1
} else if i1 == i2 {
return 0
}
return -1
}))
}
约束(Constraint)
上面的Min
可以实现功能,但是有点多余,因为你需要为每种类型(int、uint、int32和string)
定义相应的比较函数时,才能使用Min
方法。
更好的做法是定义约束Constraint
。我们知道所有整型都支持<
和>
操作符。因此,只要定义一个包含所有类型的集合,就不再需要分别为每种类型定义比较函数。
type SignedInteger interface {
~int | ~int8 | ~int16 | ~int32 | ~int64
}
然后需要修改下Vector
的实现
type SignedInteger interface {
~int | ~int8 | ~int16 | ~int32 | ~int64
}
type Vector[T SignedInteger] []T
func main() {
arr := []string{"a", "b"}
dArr := Map(arr, func(s string) string { return s + s })
fmt.Println(dArr)
v := make(Vector[int], 3)
v[0] = 1
v[1] = 2
v[2] = -1
fmt.Println(Min(v))
}
func Min[T SignedInteger](s []T) T {
r := s[0]
for _, v := range s[1:] {
if r > v {
r = v
}
}
return r
}
泛型受益
使用泛型的代码有很多方便之处。
- 编译时执行强类型检查。这非常有用,因为如果泛型代码违反了类型安全,编译器将报错。编译过程中的错误比运行时中的错误更容易修复。
看下面的例子, 这里并不是所有类型都实现==操作符,并且会报告错误。
func test[T any](s []T) T {
for _, v := range s {
if v == r {
}
}
return r
}
-
消除类型转换,使代码更丝滑,减少各种类型转换,并提高可读性。
-
减少重复的代码。可以在
GO
中实现类似Java
集合的API,同时确保类型安全。
泛型内部
泛型本质上是一种语言糖。Go
团队修改了编译器,在词法分析和语义分析阶段解析了[]操作符,然后确定是否符合泛型语法。
你可以使用go2go工具查看相关的编译代码。
go tool go2go translate xx.go2
为了进一步分析,还需要等待即将发布的官方团队的发布和更多工具。
提供一个有用的例子
和我一样,大多数开发人员肯定已经在考虑如何将泛型用到应用程序中,
同时等待发布。我的一个实践是使用它来实现类型安全映射和过滤。
package main
import (
"fmt"
)
func mapFunc[K any, V any](a []K, f func(K) V) []V {
n := make([]V, len(a), cap(a))
for i, e := range a {
n[i] = f(e)
}
return n
}
func filterFunc[K any](a []K, f func(K) bool) []K {
var n []K
for _, e := range a {
if f(e) {
n = append(n, e)
}
}
return n
}
func main() {
m := filterFunc(
mapFunc([]int{1, 2, 3, 4, 5, 6},
func(v int) int {
return v * v
},
),
func(v int) bool {
return v < 20
})
fmt.Println(m)
}