泛型函数
类型参数写在方括号里,放在普通参数之前。每个类型参数都有一个约束,规定允许哪些类型。
T 和 U 是从实参推断出来的,所以你很少需要写 Map[int, string](...)。推断依据的是函数的实参;只出现在返回值里的类型无法推断。对于 func Zero[T any]() T,你必须写 Zero[int]()。
在 Go 1.18 之前,这个函数要么为每种类型各写一遍,要么接收 []interface{},从而失去类型安全。
约束
约束就是一个接口。它限制了可以使用哪些类型,作为交换,它也告诉编译器可以对该类型的值执行哪些操作。
| 约束 | 允许的类型 | 可以使用的操作 |
|---|---|---|
any | 所有类型 | 赋值、传递、存储 |
comparable | 支持 == 的类型 | ==、!=、作为 map 的键 |
cmp.Ordered(Go 1.21) | 整数、浮点数、字符串 | <、<=、>、>=,以及 == |
| 带方法的接口 | 拥有这些方法的类型 | 调用这些方法 |
~int | ~float64 这样的类型联合 | 恰好是列出的这些类型 | 所有这些类型都支持的运算符 |
如果把 comparable 换成 any,v == target 就会编译失败:invalid operation: v == target (incomparable types in type set)。正是约束让这个运算符变得合法。
这两个辅助函数其实已经存在:slices.Index 和 slices.Max。标准库的 slices、maps 和 cmp 包涵盖了大多数日常的泛型需求,所以自己动手写之前先去查一查。
用类型联合和 ~ 自定义约束
约束可以用 | 列出多个类型。允许使用的运算符,是所有列出的类型都支持的那些。
波浪号很关键。~int64 表示“底层类型为 int64 的任意类型”,所以命名类型 Cents 也能被接受。如果写成普通的 int64,Sum([]Cents{...}) 就会失败,报错 Cents does not satisfy Number (possibly missing ~ for int64 in Number)。
包含类型联合的接口只能用作约束,不能用作普通变量的类型。var n Number 无法编译。
golang.org/x/exp/constraints 包提供了现成的 Integer、Float 和 Signed 约束。它不在标准库中,所以这里的运行环境无法导入它;标准库里对应的是 cmp.Ordered。
泛型类型
结构体、切片、map 等类型也可以有类型参数。泛型类型上的方法使用接收者的类型参数。
这个例子里有两个细节会经常遇到:
- T 的零值。 在泛型代码中,用
var zero T返回“空”值。没有适用于所有类型的T{}或nil。 - 实例化。 泛型类型在使用前必须实例化:写
Stack[string],而不是Stack。在它自己的方法里,接收者写作Stack[T]。
Go 1.24 新增了泛型类型别名:type Set[T comparable] = map[T]struct{} 现在可以编译了。
你会遇到的限制
- 方法不能有类型参数。
func (s *Stack[T]) Map[U any](...)是不允许的。改用顶层函数。 - 不支持特化。 你不能为
T = string单独写一个实现。变通办法是在函数内部对any(v)做类型 switch,而这通常说明泛型并不是合适的工具。 - 不能按名字约束运算符。 你没法说“任何带
+方法的类型”;只能在类型联合里逐一列出类型。 - 不能通过约束访问字段。 即使联合中的每个类型都有一个
ID字段,也不允许写v.ID。改为在约束里使用方法。
什么时候不该用泛型
泛型适合容器类型(栈、集合、缓存、树),以及对每种元素类型都执行相同操作的切片和 map 算法。它并非处处适用。
- 只有一种具体类型。 如果这个函数只会用
[]User调用,泛型版本只会增加阅读成本,别无好处。 - 行为随类型而不同。 这正是接口的用武之地。接收
io.Writer的函数比接收[T io.Writer]的函数更简单,效果却一样。 - 替代接口参数。
func Print[T fmt.Stringer](v T)并不比func Print(v fmt.Stringer)更好。
Go 团队自己的建议是:先针对具体类型写代码,当你发现自己第二次写同样的代码、只是换了类型时,再考虑使用类型参数。
常见错误
- 在需要
comparable或cmp.Ordered的地方用了any。 编译器会拒绝对any类型参数使用==或<。 - 忘了写
~。 像type ID int这样的自定义类型无法满足普通的int约束。 - 为类型参数返回
nil。 除非约束把 T 限定为类指针类型,否则不允许。返回零值。 - 写了一个早已存在的泛型辅助函数。 先查一查
slices、maps和cmp。
常见问题
Go 有泛型吗?
有,从 Go 1.18(2022 年 3 月)开始。函数和类型可以在方括号中声明类型参数:func Map[T, U any](s []T, f func(T) U) []U。每个类型参数都有一个约束,约束是一个接口,描述该类型必须支持什么。
Go 泛型中的 any 和 comparable 有什么区别?
any 允许所有类型,但除了赋值、传递和存储之外,你几乎不能对这些值做任何操作。comparable 只允许支持 == 和 != 的类型,这正是 map 的键或相等性判断所需要的。要使用 < 和 >,用 cmp.Ordered。
Go 约束中的波浪号(~)是什么意思?
~T 表示“底层类型为 T 的任意类型”。~int 匹配 int,也匹配 type Celsius int。如果没有波浪号,约束中的 int 只匹配 int 本身,用户自定义的类型就会被拒绝。
Go 的方法可以有类型参数吗?
不可以。方法可以使用其接收者类型的类型参数(func (s *Stack[T]) Push(v T)),但不能声明新的类型参数。改写成一个顶层泛型函数:func Map[T, U any](s *Stack[T], f func(T) U) *Stack[U]。