Menu

Golang 泛型:类型参数、约束与示例

泛型(Go 1.18+)让一个函数或类型在保持类型安全的同时适用于多种类型。学习类型参数,any、comparable 和 cmp.Ordered 约束,用 ~ 自定义约束,泛型类型,以及什么时候不该用泛型。

本页包含可运行的编辑器。编辑、运行,立即查看输出。

泛型函数

类型参数写在方括号里,放在普通参数之前。每个类型参数都有一个约束,规定允许哪些类型。

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]。

Coddy 编程语言插图

和 Coddy 一起学编程

开始