Go 泛型让一个函数适用于多种类型,由约束决定允许哪些类型。迭代器让函数把值逐个交给 for 循环,循环一 break 就立刻停下。
泛型和迭代器进入 Go 的时间相隔几年,但适合放在一篇里讲。泛型让你把函数或类型写一次,就能用于多种类型。迭代器建立在泛型之上,让普通的 for ... range 循环能遍历任何可以用函数描述的东西。
本文先讲泛型要解决的问题,再讲约束和泛型类型,然后从零开始写迭代器,最后再用标准库里现成的迭代器。下面每个程序都在 Go 1.26 上跑过,输出直接从运行结果粘贴而来。
问题:同一个函数写两遍
没有泛型时,一个把数字加起来的函数只能选一种数字类型。如果 int 和 float64 都要用,你就得写两遍:
package main
import "fmt"
func SumInts(nums []int) int {
var total int
for _, n := range nums {
total += n
}
return total
}
func SumFloats(nums []float64) float64 {
var total float64
for _, n := range nums {
total += n
}
return total
}
func main() {
fmt.Println(SumInts([]int{1, 2, 3}))
fmt.Println(SumFloats([]float64{1.5, 2.5}))
}
输出:
6
4
两个函数体一模一样,只有类型变了。修改其中一个,你都得记着同样改另一个;再来第三种数字类型,就得有第三份副本。
带类型参数的单个函数
从 Go 1.18 起,函数可以接收类型作为参数,写在普通参数前面的方括号里:
package main
import "fmt"
func Sum[T int | float64](nums []T) T {
var total T
for _, n := range nums {
total += n
}
return total
}
func main() {
fmt.Println(Sum([]int{1, 2, 3}))
fmt.Println(Sum([]float64{1.5, 2.5}))
fmt.Println(Sum[float64]([]float64{0.25, 0.5}))
}
输出:
6
4
0.75
T 是类型参数。int | float64 是它的约束:T 允许是哪些类型。在函数内部,T 和其他类型名用起来一样,所以 var total T 声明的是 T 最终所代表类型的零值。
前两次调用你没有说 T 是什么。编译器看了实参,发现是 []int,就推断出 T 一定是 int。这叫类型推断。第三次调用显式写出了类型 Sum[float64],这叫实例化。在这里它是多余的,但有时只能这么写。
用十岁孩子能懂的话说
饼干模具压进什么面团,都能压出同一个形状:巧克力的、原味的、姜饼的。你不需要为每种面团单独准备一个星形模具。
但它只对面团有用。压在石头上,什么也不会发生。约束就是模具上那张写着“适用于面团”的标签。
准确的说法
泛型函数是一个模板,编译器依据它的约束检查一次。在每个调用处,类型实参要么从普通实参推断出来,要么写在方括号里。然后编译器确认这个类型在约束的类型集里。函数体里只能做类型集中每一个类型都允许的操作:这里 T 能用 +,是因为 int 和 float64 都支持它。
这个比喻的局限:饼干模具能用混合面团压出一个形状,而泛型函数在一次调用里从不混用类型。对 []int 调用 Sum 返回的是 int。你没法传给它一个同时装着整数和浮点数的切片,因为 Go 里没有这样的切片。
推断行不通的时候
编译器只根据你传入的实参推断类型参数。如果 T 只出现在返回值里,就没有东西可以用来推断:
package main
import "fmt"
func Zero[T any]() T {
var z T
return z
}
func main() {
x := Zero()
fmt.Println(x)
}
构建失败,报错:
./main.go:11:11: in call to Zero, cannot infer T
Go 不会根据你怎么使用返回值来猜。你得写明类型:
package main
import "fmt"
func Zero[T any]() T {
var z T
return z
}
func main() {
fmt.Printf("%d %q %v\n", Zero[int](), Zero[string](), Zero[bool]())
}
输出:
0 "" false
每次实例化都得到各自类型的零值。泛型类型也会遇到同样的情况,Stack[int]{} 同样没有实参可供推断。
约束:any、comparable 和 cmp.Ordered
约束是一个接口,它决定函数体能用哪些操作。最宽的是 any,它允许所有类型,因此几乎不允许任何操作。你甚至不能用 == 比较两个 any 值,因为有些类型(比如切片)没法比较:
package main
import "fmt"
func Equal[T any](a, b T) bool {
return a == b
}
func main() {
fmt.Println(Equal(1, 1))
}
构建失败,报错:
./main.go:6:9: invalid operation: a == b (incomparable types in type set)
标准库里有两个约束覆盖了常见情况。comparable 允许所有支持 == 和 != 的类型。cmp.Ordered 来自 Go 1.21 加入的 cmp 包,允许所有支持 < 和 > 的类型:整数、浮点数和字符串。
package main
import (
"cmp"
"fmt"
)
func Index[T comparable](items []T, want T) int {
for i, item := range items {
if item == want {
return i
}
}
return -1
}
func Largest[T cmp.Ordered](items []T) T {
best := items[0]
for _, item := range items[1:] {
if item > best {
best = item
}
}
return best
}
func main() {
fmt.Println(Index([]string{"red", "green", "blue"}, "blue"))
fmt.Println(Index([]int{4, 8, 15}, 16))
fmt.Println(Largest([]int{4, 8, 15, 16}))
fmt.Println(Largest([]string{"pear", "apple", "fig"}))
}
输出:
2
-1
16
pear
选能让函数体完成工作的最窄约束。Index 只需要 ==,所以用 comparable,结构体也能用。Largest 需要 >,所以用 cmp.Ordered。标准库已经提供了这两个功能,分别是 slices.Index 和 slices.Max,用的也是同样的约束。
联合约束和 ~ 的含义
你可以把列出类型的接口定义成自己的约束,这比在每个函数上都写 int | float64 整洁。但对你自己定义的类型有个坑。下面这段看起来应该能行:
package main
import "fmt"
type Number interface {
int | float64
}
type Celsius float64
func Sum[T Number](nums []T) T {
var total T
for _, n := range nums {
total += n
}
return total
}
func main() {
temps := []Celsius{21.5, 19, 23.5}
fmt.Println(Sum(temps))
}
构建失败,报错:
./main.go:21:17: Celsius does not satisfy Number (possibly missing ~ for float64 in Number)
Celsius 基于 float64,但它是另一个类型。你声明的类型和它所基于的类型是不同的,即使两者能存的值一样。类型集 int | float64 恰好只有两个类型,Celsius 不在其中。
编译器甚至告诉了你怎么修。类型前面加 ~ 表示“这个类型,或者底层类型是它的任何类型”:
package main
import "fmt"
type Number interface {
~int | ~float64
}
type Celsius float64
func Sum[T Number](nums []T) T {
var total T
for _, n := range nums {
total += n
}
return total
}
func main() {
temps := []Celsius{21.5, 19, 23.5}
total := Sum(temps)
fmt.Printf("%v %T\n", total, total)
}
输出:
64 main.Celsius
~float64 现在包括 float64、Celsius,以及其他任何声明为 type X float64 的类型。结果仍然是 Celsius,而不是普通的 float64,所以你的类型所表达的含义还在。这就是 cmp.Ordered 每一行都带波浪号的原因:它既要接受 int,也要接受你的 type UserID int。
泛型类型:Stack[T]
不只函数,类型也可以带类型参数。栈是典型的例子,因为不管装的是什么,逻辑都一样:
package main
import "fmt"
type Stack[T any] struct {
items []T
}
func (s *Stack[T]) Push(v T) {
s.items = append(s.items, v)
}
func (s *Stack[T]) Pop() (T, bool) {
var zero T
if len(s.items) == 0 {
return zero, false
}
last := s.items[len(s.items)-1]
s.items = s.items[:len(s.items)-1]
return last, true
}
func (s *Stack[T]) Len() int {
return len(s.items)
}
func main() {
var nums Stack[int]
nums.Push(1)
nums.Push(2)
fmt.Println(nums.Pop())
fmt.Println(nums.Len())
words := Stack[string]{}
w, ok := words.Pop()
fmt.Printf("%q %v\n", w, ok)
words.Push("go")
fmt.Println(words.Pop())
}
输出:
2 true
1
"" false
go true
Stack[int] 和 Stack[string] 是两个不同的类型。每个方法都在接收者里写上 Stack[T],方法里的 T 就是创建这个栈时用的类型。Pop 返回 comma-ok 形式的一对值,和 map 用的是同一种模式,所以空栈返回 0 false 或 "" false,而不是 panic。
方法不能声明自己的类型参数,只能使用类型上的:
package main
import "fmt"
type Stack[T any] struct {
items []T
}
func (s *Stack[T]) Map[U any](f func(T) U) []U {
return nil
}
func main() {
fmt.Println(Stack[int]{})
}
构建失败,报错:
syntax error: method must have no type parameters
需要这种功能时,改写成普通函数:func Map[T, U any](s *Stack[T], f func(T) U) []U。
什么时候不该用泛型
泛型适合那些对各种类型都完全相同、只是搬运值的代码:容器、Sum、Index、Largest。如果代码要调用值上的方法,用接口通常更简单。
比如一个打印任何带 String 方法的值的函数。你可以写 func Show[T fmt.Stringer](v T)。但 func Show(v fmt.Stringer) 能做同样的事,读起来更直白,而且早在泛型出现之前就是 Go 的常规写法。类型参数在这里没有任何用处,因为函数体只调用了 v.String()。
大致的判断方法:
- 如果你要为几种类型写同样的函数体,只有类型名不同,就用类型参数。
- 如果每种类型通过方法各做各的事,就用接口。
- 如果目前只有一种类型,就直接用这个类型。等第二种出现时再改成泛型也不迟。
迭代器:逐个交出值的函数
从 Go 1.23 起,for ... range 循环可以遍历函数。这样的函数叫迭代器。它接收一个参数,一个按惯例命名为 yield 的回调函数,并为每个值调用它一次:
package main
import (
"fmt"
"iter"
)
func Countdown(n int) iter.Seq[int] {
return func(yield func(int) bool) {
for i := n; i > 0; i-- {
if !yield(i) {
return
}
}
}
}
func main() {
for n := range Countdown(3) {
fmt.Println(n)
}
fmt.Println("liftoff")
}
输出:
3
2
1
liftoff
iter.Seq[int] 是 iter 包里的泛型类型。它只是 func(yield func(int) bool) 的一个名字。Countdown 自己什么都不数。它返回一个函数,循环要值时,那个函数才去数。
迭代器每调用一次 yield(i),循环体就以 n 等于 i 运行一次。迭代器函数返回时,循环结束。
用十岁孩子能懂的话说
迭代器就是一台自动售货机。每按一次按钮,它给你一件东西。它不会一下子把全部存货倒在地上,也不需要知道你想要几件。
你还可以随时走开。售货机发现没人再按按钮,就不再往外出货了。
准确的说法
循环体变成了 yield 函数。如果循环还想要下一个值,yield 返回 true;如果循环已经提前结束(通过 break、return 或用 goto 跳出循环),就返回 false。迭代器必须检查这个 bool,一看到 false 就立即返回。range 循环一直运行到迭代器函数返回为止。
这个比喻的局限:真正的售货机是你按下按钮,准备好了再取下一件。Go 的迭代器正好反过来。做主的是售货机。它每件东西调用你一次,也就是调用你的循环体,你只能回答“还要”或“停”。这是推,不是拉,这也是 iter.Pull 函数存在的原因,最后一节会讲到。
break 让 yield 返回 false
观察迭代器本身,就能看出 false 起什么作用。这里它在每个值之前打印一行,停下时再打印一行:
package main
import (
"fmt"
"iter"
)
func Countdown(n int) iter.Seq[int] {
return func(yield func(int) bool) {
for i := n; i > 0; i-- {
fmt.Println("iterator: sending", i)
if !yield(i) {
fmt.Println("iterator: loop said stop")
return
}
}
fmt.Println("iterator: ran out")
}
}
func main() {
for n := range Countdown(5) {
fmt.Println("loop: got", n)
if n == 4 {
break
}
}
fmt.Println("after the loop")
}
输出:
iterator: sending 5
loop: got 5
iterator: sending 4
loop: got 4
iterator: loop said stop
after the loop
迭代器发出 5,拿回 true。它发出 4,循环体执行到 break,所以这次 yield 调用返回 false。迭代器打印消息后返回,然后才轮到 after the loop 运行。3、2、1 它根本没有算。
如果迭代器无视 false,继续调用 yield,Go 不会悄悄地再跑一遍你的循环体,而是直接 panic:
package main
import "fmt"
func Countdown(n int) func(func(int) bool) {
return func(yield func(int) bool) {
for i := n; i > 0; i-- {
yield(i)
}
}
}
func main() {
for n := range Countdown(3) {
fmt.Println(n)
if n == 2 {
break
}
}
}
它输出两行,然后停下:
3
2
panic: runtime error: range function continued iteration after function for loop body returned false
这个版本也说明了 iter.Seq 只是一个名字。普通的 func(func(int) bool) 在 range 循环里一样能用。
接收迭代器的迭代器
迭代器是一个值,所以函数可以接收一个迭代器,再返回另一个。Filter 只传递通过检验的值:
package main
import (
"fmt"
"iter"
)
func Countdown(n int) iter.Seq[int] {
return func(yield func(int) bool) {
for i := n; i > 0; i-- {
if !yield(i) {
return
}
}
}
}
func Filter[T any](seq iter.Seq[T], keep func(T) bool) iter.Seq[T] {
return func(yield func(T) bool) {
for v := range seq {
if keep(v) && !yield(v) {
return
}
}
}
}
func main() {
even := func(n int) bool { return n%2 == 0 }
for n := range Filter(Countdown(10), even) {
fmt.Println(n)
}
}
输出:
10
8
6
4
2
Filter 是泛型的,所以适用于任何元素的序列。在内部,它用普通循环遍历输入序列。当它自己的调用方停下时,yield 返回 false,Filter 返回,这也结束了它的内层循环,进而让 Countdown 的 yield 返回 false。最上层的一个 break 会沿着整条链一路传到底。
每步两个值:iter.Seq2
有些序列天生是成对交出值的,比如索引和值,或者键和值。这时用 iter.Seq2[K, V],它就是 func(yield func(K, V) bool),range 循环会得到两个变量。slices 和 maps 包两种都会返回:
package main
import (
"fmt"
"maps"
"slices"
"strings"
)
func main() {
fruits := []string{"pear", "apple", "fig"}
for i, f := range slices.All(fruits) {
fmt.Println(i, f)
}
for f := range slices.Values(fruits) {
fmt.Println(strings.ToUpper(f))
}
stock := map[string]int{"pear": 3, "apple": 0, "fig": 12}
names := slices.Collect(maps.Keys(stock))
fmt.Println(len(names))
fmt.Println(slices.Sorted(maps.Keys(stock)))
}
输出:
0 pear
1 apple
2 fig
PEAR
APPLE
FIG
3
[apple fig pear]
slices.All 是 iter.Seq2[int, string]:索引和值,和直接遍历切片一样。slices.Values 是只有值的 iter.Seq[string]。maps.Keys 是遍历键的 iter.Seq[string],顺序和平常遍历 map 一样不可预测,所以程序只打印 slices.Collect 收集了多少个键,而不打印切片本身。
最后一行你在讲流程控制和讲 map 的那两部分里见过。现在你能完整读懂它了。maps.Keys(stock) 并不构建切片,而是返回一个迭代器。slices.Sorted 遍历这个迭代器,把每个值收集到一个新切片里,排好序后返回。slices.Sorted 接收 iter.Seq[E],其中 E 满足 cmp.Ordered,所以本文的前后两半在这一行里会合了。
直接遍历切片或 map 仍然是常规做法。当你想把序列传给别的东西,比如 slices.Sorted 或你自己的 Filter 时,这些函数才派上用场。
用 iter.Pull 拉取值
有时你确实想自己按按钮,在循环之外一次取一个值。iter.Pull 把推式迭代器变成一个 next 函数和一个 stop 函数:
package main
import (
"fmt"
"iter"
)
func Countdown(n int) iter.Seq[int] {
return func(yield func(int) bool) {
for i := n; i > 0; i-- {
if !yield(i) {
return
}
}
}
}
func main() {
next, stop := iter.Pull(Countdown(2))
defer stop()
for range 3 {
v, ok := next()
fmt.Println(v, ok)
}
}
输出:
2 true
1 true
0 false
每次调用 next 都返回下一个值和 true;迭代器结束后,返回零值和 false。第三次调用得到 0 false,和 Stack.Pop 是同样的 comma-ok 形式。
调用 stop 是告诉迭代器你用完了,于是它的 yield 返回 false,它就可以做清理工作。defer stop() 确保即使你提前不再拉取,这件事也会发生。需要并排逐步遍历两个序列时,比如比较它们,就该用 iter.Pull,单个 range 循环做不到这一点。
要点
- 类型参数让你把函数或类型写一次,用于多种类型。编译器通常能从实参推断它。推断不了时,写在方括号里:
Zero[int]()。 - 约束是允许的类型集合,它决定函数体能做什么。先用
any、comparable或cmp.Ordered,不够再自己定义。 ~float64表示底层类型是float64的任何类型。没有波浪号,你的type Celsius float64就被排除在外。- 方法不能有自己的类型参数。函数体要调用值上的方法时,接口往往是更简单的选择。
- 迭代器是为每个值调用一次
yield的函数。yield返回false说明循环已经停下,迭代器必须返回。 maps.Keys和slices.All返回迭代器,slices.Collect和slices.Sorted把迭代器变回切片。
迭代器不会交给你一个列表。它为每个值调用你一次,直到你说停。