golang泛型机制
What is Generic Mechanism
面向对象的一个重要目标就是对代码复用的支持。支持这个目标的一个重要机制就是
泛型机制(generic mechanism): 如果除去对象的基本类型外,实现方法是相同的,那么我们就可以用泛型实现(generic implementation) 来描述这种基本的功能。
GOlang 在1.18之后的版本开始支持泛型了.
函数要支持泛型机制,需要有2个前提:
- 对于函数而言,需要一种方式来声明这个函数到底支持哪些类型的参数
- 对于调用者而言,需要一种方式去指定给函数传递的参数是什么类型
为了满足以上前提条件:
- 在声明函数的时候,除了需要像普通函数一样添加函数的形式参数之外, 还要声明这些形参的类型决定因素(type parameters), 从而让函数支持泛型机制,处理不同类型的参数
- 在函数调用时,除了像普通函数一样传递实参之外,还需要传递泛型函数的类型决定因素的对应类型实参(type arguments)
每个类型决定因素都有类型约束(type contraint), 有点像是类型决定因素的元类型(meta-type), 即约束类型决定因素可以传入什么类型的类型实参(e.g. int string float etc.)
注意:一定要确保泛型实现代码里对类型决定因素所做的所有操作都是合法的。例如对一个包含了string
类型约束的类型决定因素进行取下标操作,如果这时类型约束还包括了int
等数字类型的话,编译器就会报错,从而编译失败。
Generic Implementation
泛型函数实现
- 使用类型约束来允许value为
int
类型和float
类型的map作为函数的类型实参.1
2
3
4
5
6
7
8
9// SumIntsOrFloats sums the values of map m. It supports both int64 and float64
// as types for map values.
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
作为类型决定因素, 一个函数形参m
, 类型为map[K]V
, 函数返回值的类型为V
。 - 类型决定因素
K
的类型约束为comparable
。comparable
是golang新引入的预定义标识符,是一个接口,指代可以使用==
或!=
来进行比较的类型集合。GOlang中map的key的类型决定因素必须是comparable
的,这确保了调用方使用合法的类型作为map的key。glang spec对map的key有如下约束:
- key类型必须定义比较操作符
==
和!=
- key类型必须不能是
function
、map
或slice
(没有定义比较操作符) - 对于
interface
类型,其动态类型必须定义比较操作符 - 不满足上述约束,则会导致运行时异常(run-time panic)
- 类型决定因素
V
的类型约束为int64
或float64
。使用|
表示取并集, 即int64
和float64
的任意一个都可以满足类型约束,作为函数传入的类型实参。 - 函数参数m的类型是
map[K]V
。因为K
是comparable
类型, 所以map[K]V
是一个合法的map类型。如果K
没有声明为comparable
, 则编译器会拒绝对map[K]V
的引用。
- key类型必须定义比较操作符
- 在
main
函数中调用上面的泛型实现在调用泛型函数时,可以通过
[]
传入类型实参给类型决定因素。
也可省略掉类型实参,让编译器根据函数调用时传入的函数实参类型自动推导出来, 从而让代码更加简洁。
注意:类型实参的自动推导并不是永远可行的。比如你调用的泛型函数没有形参,不要传递实参,那编译器就不能自动推导,需要在[]
中显示指定类型实参。
1 | func main() { |
运行结果:
1 | Generic Sums: 46 and 62.97 |
Declaration type contraint
把泛型函数里的类型约束以接口(interface
)的形式做定义,这样类型约束就可以在很多地方被复用。声明类型约束可以帮助精简代码,特别是在类型约束很复杂的场景下。
我们可以声明一个类型约束(type constraint)为接口(interface
)类型。这样的类型约束可以允许任何实现了该接口的类型作为泛型函数的类型实参。例如,你声明了一个有3个方法的类型约束接口,然后把这个类型约束接口作为泛型函数的类型决定因素的类型,那函数调用时的类型实参必须要实现了接口里的所有方法。
类型约束接口也可以指代特定类型,在下面大家可以看到具体使用。
把原本来函数声明里的 int64
和float64
的并集改造成了一个新的类型限制接口Number
,当我们需要限制类型参数为int64
或float64
时,就可以使用Number
这个类型限制来代替int64 | float64
的写法。
1 | type Number interface { |
在main
函数中添加以下代码
1 | fmt.Printf("Generic Sums with Constraint: %v and %v\n", |
运行结果如下
1 | Generic Sums: 46 and 62.97 |
Generics tricks
对任何类型都返回零值
你经常会写一些返回 any
和 error
的代码,比如说下面这样
1 | func Do[V any](v V) (V, error) { |
假设你在这里写return 0
, err
。这将是一个编译错误。原因是any
类型可以是int
类型以外的类型,比如string
类型。那么我们应该怎么做呢?
让我们用类型参数的V
声明一次变量。然后你可以把它写成可编译的形式,如下:
1 | func Do[V any](v V) (V, error) { |
此外,可以使用带命名的返回值来简化单行的书写。
1 | func Do[V any](v V) (ret V, _ error) { |
利用泛型定义python里的字典
在golang中map被定义为一种由键(key)及索引值(value)构成的元素集合,实质上与python中Dictionary表达的是同一个东西。考虑到函数式编程中经常使用map来表示映射,在有些场景下我们可以通过将golang中的map重定义为Dictionary来减少概念上的混淆。
1 | package main |
利用泛型定义python里的集合Set
Golang并未提供内置的Set类型,不过一般我们可以利用map的key的唯一性来自定义Set类型。
1 | package sets |
RERFERENCES
[1] https://go.dev/doc/tutorial/generics
[2] YuanJianzheng. https://juejin.cn/post/7055992040068218888
[3] https://zhuanlan.zhihu.com/p/438252333