Go语言的1.18版本还未正式发布,但我们已经可以用它的RC版本尝试一些新特性。今天来聊聊Go 1.18里的范型(Generics)。
安装
Generics是1.18的新特性,在目前的发布版1.17里不支持,因此需要手动安装RC版:
go install golang.org/dl/go1.18rc1@latest
安装完后,你的GOPATH
或者GOBIN
里会出现一个go1.18rc1
的可执行文件。然后下载SDK:
go1.18rc1 download
此后编译和运行go代码,需要使用这个可执行文件,或者你也可以用alias把go指向这个RC版本,这样就不用每次打版本号了:
alias go=go1.18rc1
使用Generics
看一个简单的例子,假如我们要实现一个函数,输入是一个名字(string)到整数数值(int64)的映射表,返回是这个映射表里所有数值的总和:
func SumInts(m map[string]int64) int64 {
var s int64
for _, v := range m {
s += v
}
return s
}
很简单的实现。但是,如果我们想要也能处理浮点数值(float64)呢?是否要写个类似的函数:
func SumFloats(m map[string]float64) float64 {
var s float64
for _, v := range m {
s += v
}
return s
}
这样做也不是不行,但是有很多重复代码,使用起来也不方便。
看看用Generics的实现:
func SumIntsOrFloats[K comparable, V int64 | float64](m map[K]V) V {
var s V
for _, v := range m {
s += v
}
return s
}
解释一下:
-
方括号里的部分,声明了类型参数:
[K comparable, V int64 | float64]
,这里声明了两个类型,K是内置的comparable类型,任何类型只要能进行比较(==, !=)就属于comparable;V可以是int64或float64之一,也就是这两个类型的union,V这样的又称为“类型限制”(type constraint),因为它限制了V可能的类型。 -
然后在函数的参数部分,用这两个类型K和V来声明参数:
m map[K]V
,也就是说输入参数是一个类型K到类型V的映射表。因为map要求key必须是可比较的,这里K类型声明为comparable就保证了这一点。
-
函数的返回值,也是类型V。
这样,我们只需要一个实现就能支持多种类型了。
如何调用这个范型函数?也很简单:
ints := map[string]int64{
"first": 12,
"second": 34,
}
floats := map[string]float64{
"first": 12.34,
"second": 22.55,
}
fmt.Printf("SumInts: %v\n",
SumIntsOrFloats[string, int64](ints))
fmt.Printf("SumFloats: %v\n",
SumIntsOrFloats[string, float64](floats))
在这个例子里,我们调用范型函数的时候加上了类型参数,例如[string, int64]
。实际上,这个类型参数在很多时候是可以省略的,Go会根据函数的参数自动推导类型:
fmt.Printf("SumInts: %v\n",
SumIntsOrFloats(ints))
fmt.Printf("SumFloats: %v\n",
SumIntsOrFloats(floats))
当然这种自动类型推导并不是在所有情况下都可用,比如如果函数没有参数,那就没法推导了,调用时必须加上类型参数。
另一个比较好的习惯是,把范型函数的类型参数提取到interface里,可以提高代码的可读性和可维护性:
type Number interface {
int64 | float64
}
func SumNumbers[K comparable, V Number](m map[K]V) V {
var s V
for _, v := range m {
s += v
}
return s
}
这里把V的类型提取到Numbers这个interface里,Numbers里定义了类型限制(int64或float64)。这样范型函数SumNumbers就更简洁,并且如果还有其他的范型函数用到它的话,我们不用到处复制 int64 | float64
,可维护性大大提高。
Go 1.18的Generics特性就先聊到这里。感谢阅读!
文章评论