Go语言 泛型
Go1.18正式支持泛型,本文参考官方文档中关于Go泛型的入门文档抽取泛型编程的重点并适当补充。
官方文档:Tutorial: Getting started with generics - The Go Programming Language
本文为自存的学习笔记,希望也能对你有所帮助。
什么是泛型?
维基百科中对泛型定义如下:
泛型程序设计(generic programming)是程序设计语言的一种风格或范式。泛型允许程序员在强类型程序设计语言中编写代码时使用一些以后才指定的类型,在实例化时作为参数指明这些类型。各种程序设计语言和其编译器、运行环境对泛型的支持均不一样。Ada、Delphi、Eiffel、Java、C#、F#、Swift 和 Visual Basic .NET 称之为泛型(generics)
;ML、Scala和 Haskell称之为参数多态(parametric polymorphism)
;C++ 和 D称之为模板
。具有广泛影响的1994年版的《Design Patterns》一书称之为参数化类型(parameterized type)
。泛型编程 - 维基百科,自由的百科全书 (wikipedia.org)
简单地说泛型所做的事情就是声明一个函数用来处理不同类型的参数。官方文档中是这样说的:
With generics, you can declare and use functions or types that are written to work with any of a set of types provided by calling code.
通过泛型,你可以声明和使用泛型函数,在调用函数的时候,允许使用不同类型的参数作为函数实参。
为了更好理解,提前介绍一下文档中使用的例子,在文档中主要做了下面三件事。
- 编写一个函数用于计算一堆
整数
(int64)的和,简单记作函数A。 - 编写另一个函数用于计算一堆
浮点数
(float64)的和,简单记作函数B。 - 编写一个使用泛型的函数,他可以计算整数的和也可以计算浮点数的和,记作函数C。
暂时不需要理解怎么实现函数C,这里只需明白,一个函数C包含了函数A和函数B的功能。原本两个函数才能达成目的,有了泛型之后一个函数就可以搞定。当参数类型更多时,使用泛型就可以极大的减少代码量和工作量。
不使用泛型(Non-Generic)
编写代码实现两个函数分别用于整数累加和浮点数累加。
// SumInts adds together the values of m.
func SumInts(m map[string]int64) int64 {
var s int64
for _, v := range m {
s += v
}
return s
}
// SumFloats adds together the values of m.
func SumFloats(m map[string]float64) float64 {
var s float64
for _, v := range m {
s += v
}
return s
}
在main函数中调用该函数
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))
}
运行结果为Non-Generic Sums: 46 and 62.97
至此,分别为map[string]int64
和map[string]int64
类型编写一个函数用于累加。
使用泛型(Generic)
利用泛型
编写一个函数
代替上述的两个函数。
类型参数
在声明使用泛型的函数时,需要特定语法声明该函数支持的类型
。与此对应的,调用该函数时也需要指明实参的类型
。
为满足该要求,在函数声明时除函数参数
(function parameters)之外需要额外声明类型参数
(type parameters)。与此对应的,调用该函数不仅仅需要传递实参(对应函数参数),也需要额外指定类型实参(对应类型参数)。
简单说明一下,在介绍泛型之前,我们声明函数时定义的参数即为普通函数参数。如
func sum(a, b int) int { return a + b }
这里a, b 便是普通函数参数(形参)。类型参数的声明方法在后面使用泛型的函数声明中介绍。
调用函数时只需要传递两个实参即可。
fmt.Println(sum(1, 2))
类型约束
每个类型参数都有一个类型约束
,作为类型参数的一种元类型。每个类型约束都指定了调用代码可以为各自的类型参数使用的允许的类型参数。虽然一个类型参数的约束通常代表一组类型,但在编译时,类型参数代表一个单一的类型–由调用代码提供的类型参数。如果类型参数的类型不被类型参数的约束所允许,代码就不会被编译。
切记
:一个类型参数一定要支持代码里对该类型所做的所有操作。例如你的函数代码试图对某个类型参数执行string
操作,比如按照下标索引取值,但是这个类型参数的类型限制包括了数字类型,那代码就会编译失败。
声明泛型函数
func SumIntsOrFloats[K comparable, V int64 | float64](m map[K]V) V {
var s V
for _, v := range m {
s += v
}
return s
}
-
声明函数
SumIntsOrFloats
,它有2个类型参数K
和V
(在一对中括号中声明类型参数),一个函数参数m
,类型是map[K]V
,返回返回类型是V
。 -
类型参数K的类型限制是
comparable
。comparable
限制是Go里预声明的。它可以接受任何能做```和!=
操作的类型。Go语言里map的key必须是comparable
的,因此类型参数K的类型限制使用comparable
是很有必要的,这也可以确保调用方使用了合法的类型作为map的key。 -
类型参数V的类型限制是
int64
和float64
的并集,|表示取并集,也就是int64
和float64
的任一个都可以满足该类型限制,可以作为函数调用方使用的类型实参。 -
函数参数m的类型是
map[K]V
。我们知道map[K]V
是一个合法的map类型,因为K是一个comparable的类型。如果我们不声明K为comparable,那编译器会拒绝对map[K]V
的引用。 -
简单总结
:与普通函数相比, 声明泛型函数时就是在[]中先声明类型参数与类型约束,然后在声明函数参数与返回值类型时直接使用先前声明的类型参数代替普通函数中的具体类型。-
泛型函数 VS 普通函数
func foo(a int, b int) int { doSomething... } // 普通函数, 为参数a, b 以及返回值指明类型为int // 泛型函数, 中括号中声明了K V类型, 在为参数m指明类型为 map[K]V(包括多种具体类型,如map[string]int64, map[string]float64等), 为返回值指定为V类型. func bar[K comparable, V int64 | float64](m map[K]V) V { doSomething... }
-
调用泛型函数
main函数中调用声明的泛型函数
fmt.Printf("Generic Sums: %v and %v\n",
SumIntsOrFloats[string, int64](ints), // 指定类型参数K为string, 类型参数V为int64
SumIntsOrFloats[string, float64](floats)) // 指定类型参数K为string, 类型参数V为float64
在这段代码里:
-
调用了上面定义的泛型函数,传递了2种类型的map作为函数的实参。
-
函数调用时
指明了类型实参
(方括号[ ]里面的类型名称),用于替换调用的函数的类型实参。在接下来的内容里,你会经常看到调用函数时,会
省略掉类型实参
,因为Go通常(不是一定)可以根据你的代码推断出类型实参。 -
打印函数的返回值。
调用泛型函数的时候移除类型实参
上面提到了在调用泛型函数时经常会忽略掉类型实参
。这是因为编译器可以自动推导出来,编译器是根据函数调用时传的函数实参
类型做的推导判断。
注意:类型实参的自动推导并不是永远可行的。比如,你调用的泛型函数没有函数形参
,不需要传递函数实参
,那编译器就不能根据函数实参
自动推导,需要在函数调用显式地在方括号[]里指定类型实参
。
PS:注意区分类型实参(type argument)、类型形参(type parameter)、函数实参(function parameter 或 paramter)、函数形参(function argument 或 argument)。
简易调用
在main函数中用简易方式调用泛型函数
fmt.Printf("Generic Sums, type parameters inferred: %v and %v\n",
SumIntsOrFloats(ints), // 对比之前的调用方法, 省略了[]以及其中类型实参
SumIntsOrFloats(floats))
运行代码
$ go run .
Non-Generic Sums: 46 and 62.97
Generic Sums: 46 and 62.97
Generic Sums, type parameters inferred: 46 and 62.97
声明类型限制(type constraint)
在最后这个章节,我们会把泛型函数里的类型限制以接口(interface)的形式做定义,这样类型限制就可以在很多地方被复用。声明类型限制可以帮助精简代码,特别是在类型限制很复杂的场景下。
我们可以声明一个类型限制(type constraint)为接口(interface)类型。这样的类型限制可以允许任何实现了该接口的类型作为泛型函数的类型实参。例如,你声明了一个有3个方法的类型限制接口,然后把这个类型限制接口作用于泛型函数的类型限制,那函数调用时的类型实参必须要实现了接口里的所有方法。
类型限制接口也可以指代特定类型,在下面大家可以看到具体使用。
代码实现
-
在
main
函数上面,import
语句下面,添加如下代码用于声明一个类型限制type Number interface { int64 | float64 }
在这段代码里,我们
- 声明了一个名为Number的接口类型用于类型限制
- 在接口定义里,声明了int64和float64的并集
我们把原本来函数声明里的 int64和float64的并集改造成了一个新的类型限制接口Number,当我们需要限制类型参数为int64或float64时,就可以使用Number这个类型限制来代替
int64 | float64
的写法。 -
在已有的函数下面,添加一个新的SumNumbers泛型函数
// SumNumbers sums the values of map m. Its supports both integers // and floats as map values. func SumNumbers[K comparable, V Number](m map[K]V) V { var s V for _, v := range m { s += v } return s }
在这段代码里
- 我们定义了一个新的泛型函数,函数逻辑和之前定义过的泛型函数
SumIntsOrFloats
完全一样,只不过对于类型参数V,我们使用了Number来作为类型限制。和之前一样,我们把类型参数用于函数形参和函数返回类型。
- 我们定义了一个新的泛型函数,函数逻辑和之前定义过的泛型函数
-
在
main.go
已有代码后面,添加如下代码fmt.Printf("Generic Sums with Constraint: %v and %v\n", SumNumbers(ints), SumNumbers(floats))
在这段代码里
-
我们对2个map都调用
SumNumbers
,打印每次函数调用的返回值。和上面一样,在这个泛型函数调用里,我们忽略了类型实参(方括号[]里面的类型名称),Go编译器根据函数实参进行自动类型推导。
-
代码运行
在main.go
所在目录下,运行如下命令:
$ go run .
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
结论
泛型简单地说就是定义一个通用于多种不同类型的函数。
简单总结一下在Go中使用泛型函数的重点。
- 声明函数以及调用函数时额外定义类型形参(type parameter)和类型实参(type argument)。
在调用泛型函数时常常利用编译器自动推断类型参数,但需要注意这并不是永远可行的
- 泛型函数声明中每个类型形参都对应一个类型约束。类型约束通常以接口的形式表现(
方法
或直接限制规定类型
)
参考资料
Tutorial: Getting started with generics - The Go Programming Language