本文介绍了Go中泛型的基础知识。使用泛型,你可以声明和使用具有为调用代码提供的一组类型的函数或类型。在下面,我们将声明两个简单的非泛型函数,然后我们用一个泛型函数来实现相同的功能。
我们将完成以下几个部分:
这里我就将文件夹命名为 generics ,然后我们进入到该文件夹并初始化模块
cd generics
go mod init example/generics
在这一步中,我们将添加两个函数,每一个函数将map中的数相加并返回。
main.go
package main
func SumInts(m map[string]int64) int64 {
var s int64
for _, v := range m {
s += v
}
return s
}
func SumFloats(m map[string]float64) float64 {
var s float64
for _, v := range m {
s += v
}
return s
}
上面的代码中的两个方法分别是对都以字符串为key,分别以int64和float64为value的数进行求和并返回对应的数据。下面我们编写主方法并在里面初始化两个map然后调用上面的方法
func main() {
ints := map[string]int64{
"first": 34,
"second": 12,
}
floats := map[string]float64{
"first": 35.98,
"second": 26.99
}
fmt.Printf("Non-Generic Sums: %v and %v\n", SumInts(ints), SumFloats(floats))
}
由于上面我们使用了 fmt 进行打印,因此需要引入 fmt 标准库
package main
import "fmt"
使用 go run .
命令运行代码
go run .
运行结果
Non-Generic Sums: 46 and 62.97
上面使用了两个方法实现类类似的功能,只不过它们的参数类型不同,如果一两个这样的操作你觉得无所谓,但是当数据类型很多时如果实现的功能相同那上面的做法很不优雅,会产生大量的冗余代码,下面我们将使用一个方法来实现上面的功能,该方法允许int64或float64作为参数类型
func SumIntsOrFloats[K comparable, V int64 | float64](m map[K]V) V {
var s V
for -, v := range m {
s += v
}
return s
}
上面的代码中声明了一个SumIntsOrFloats函数,该函数具有两个类型参数(方括号里面的)K和V,以及一个使用类型参数的形参集map[K]V的m,该函数返回V类型的值。
给K指定了 comparable
类型的参数,Go要求map的Key值具有可比性,因此将它设置为可比较的类型是必要的,这还确保了调用者对map简直的类型使用。
给V指定了两种类型的并集,即int64和float64,只要符合其中的一种类型即可,形参m指定为了map[K]V,这里我们确定map是一个有效的map,因为在前面已经对K做了可比较类型的约束。下面我们修改主方法:
fmt.Printf("Generic Sums: %v and %v\n",
SumIntsOrFloats[string, int64](ints),
SumIntsOrFloats[string, float64](floats))
go run .
运行结果
Generic Sums: 46 and 62.97
可以看到,是同样的结果,但是我们使用了泛型函数只需要一个函数就可以。
在上面的main方法中我们在调用泛型方法时加了参数类型,也就是中括号里面的内容[string, int64]
来告诉泛型函数我们调用时传递的参数类型,但其实Go编译器在编译的时候是可以根据方法参数推断出参数的类型,因此我们可以省略不写。但是注意:这并不总是可能的,如果被调用的泛型函数没有参数,此时我们在调用方法的时候应该包含参数类型。
下面我们修改一下main.go的代码:
fmt.Printf("Generic Sums, type parameters inferred: %v and %v\n",
SumIntsOrFloats(ints),
SumIntsOrFloats(floats))
下面我们运行代码,这次我把上面的两个打印都放开,对比一下,结果是:
Non-Generic Sums: 46 and 62.97
Generic Sums: 46 and 62.97
Generic Sums, type parameters inferred: 46 and 62.97
可以看到,当我们省略了参数类型的时候依然正确的运行了程序,说明Go编译的时候自动推断出了参数的类型。
下面,我们将把前面定义的约束移动到我们自己的接口中,这样我们就可以在多个地方重用它,这种声明方式有助于简化代码,例如当约束更复杂时。
当我们将类型约束声明为接口,约束允许实现接口的任何类型。例如,如果用三个方法声明类型约束接口,然后将其与泛型函数中的类型参数一起使用,则用于调用该函数的类型参数被许具有所有的这些方法。
约束接口也可以引用特定的类型,例如我们下面这样使用
type Number interface {
int64 | float64
}
在上面的代码中,我们声明了一个名为Number的约束接口,在接口内声明了int64和float64的并集,下面当我们需要使用`int64 | float64`这样的约束类型时就可以使用Number这个类型,而不需要写`int64 | float64`代码如下
~~~go
func SumNumbers[K comparable, V Number](m map[K]V) V {
var s V
for _, v := range m {
s += v
}
return s
}
然后我们将main.go中添加新方法的打印
fmt.printf("Generic Sums with Constraint: %v and %v\n",
SumNumbers(ints),
SumNumbers(floats))
然后我们运行代码,结果如下:
Non-Generic Sums: 46 and 62.97
Generic Sums: 46 and 62.97
Generic Sums, type parameters inferred: 46 and 62.97
Generic Sums with Constraint: 46 and 62.97
package main
import "fmt"
type Number interface {
int64 | float64
}
//Non-Generic Function
func SumInts(m map[string]int64) int64 {
var s int64
for _, v := range m {
s += v
}
return s
}
func SumFloat(m map[string]float64) float64 {
var s float64
for _, v := range m {
s += v
}
return s
}
// Generic Function
func SumIntsOrFloats [K comparable, V int64 | float64](m map[K]V) V {
var s V
for _, v := range m {
s += v
}
return s
}
func SumNumbers [K comparable, V Number](m map[K]V) V {
var s V
for _, v := range m {
s += v
}
return s
}
func main() {
ints := map[string]int64 {
"first": 34,
"second": 12,
}
floats := map[string]float64 {
"first": 35.98,
"second": 26.99,
}
fmt.Printf("Non-Generic Sums: %v and %v\n", SumInts(ints), SumFloat(floats))
fmt.Printf("Generic Sums: %v and %v\n", SumIntsOrFloats[string, int64](ints), SumIntsOrFloats[string, float64](floats))
fmt.Printf("Generic Sums, type parameters inferred: %v and %v\n", SumIntsOrFloats(ints), SumIntsOrFloats(floats))
fmt.Printf("Generic Sums with Constraint: %v and %v\n", SumNumbers(ints), SumNumbers(floats))
}