range 是 for 的一种形式,用来遍历一个集合。每次迭代它给你一个或两个值,具体是什么值取决于你遍历的类型:
| 遍历对象 | 第一个值 | 第二个值 | 说明 |
|---|---|---|---|
slice []T、数组 [n]T | 索引 int | 元素 T,是副本 | |
数组指针 *[n]T | 索引 | 元素 | |
| 字符串 | 字节索引 int | rune | 按 UTF-8 解码 |
map map[K]V | 键 K | 值 V | 顺序随机 |
channel chan T | 元素 T | 无 | 直到 channel 关闭 |
整数 n | 0 到 n 减 1 | 无 | Go 1.22 |
func(yield func() bool) | 无 | 无 | Go 1.23 |
func(yield func(V) bool) | V | 无 | Go 1.23 |
func(yield func(K, V) bool) | K | V | Go 1.23 |
选择要接收哪些变量
for i, v := range s {} // both
for i := range s {} // index (or key) only
for _, v := range s {} // value only
for range s {} // neither: just repeat len(s) times
声明了却不使用的变量会导致编译错误,所以不需要的变量要么省略,要么用 _ 丢弃。for range 3 {} 是把某件事重复三次的最短写法。
值是一个副本
每次迭代,元素都会被复制到值变量中。修改这个变量不会改变集合:
当元素是很大的结构体时,复制也会耗费时间。对于由大结构体组成的 slice,用 for i := range s 配合 s[i] 可以避免复制。
字符串返回 rune
遍历字符串时会按 UTF-8 解码。第一个值是每个字符起始位置的字节偏移量,所以它可能会跳过一些数字:
如果要按字节而不是按字符遍历,使用索引循环 for i := 0; i < len(s); i++,或者遍历 []byte(s)。UTF-8 的细节参见 rune 与 byte 一文。
map:顺序随机
map 的遍历顺序是未定义的,运行时还会故意打乱它,让代码无法依赖某一种顺序。如果需要确定的输出,就对键排序:
遍历时删除条目是安全的,被删除且尚未遍历到的条目不会再出现。添加条目是允许的,但结果不可预测:新键在同一个循环里之后可能出现,也可能不出现。fmt.Println 打印 map 时会对键排序,所以最后一行的输出是稳定的。
channel:直到关闭
遍历 channel 会一直接收值,直到 channel 被关闭并且取空。如果没人关闭它,循环就会永远阻塞(如果所有 goroutine 都被阻塞,运行时会以 fatal error: all goroutines are asleep - deadlock! 终止):
关闭 channel 的应该是发送方,而不是接收方。带缓冲的 channel 和 select 参见 channel 一文。
整数(Go 1.22)
range n 从 0 数到 n,但不包括 n。循环变量的类型就是 n 的类型:
for i := range 3 {} // i is int: 0, 1, 2
for i := range uint8(3) {} // i is uint8
for range 0 {} // runs zero times
n 为负数时,循环同样执行零次。n 只在循环开始前求值一次。
range 求值的内容和时机
range 后面的表达式在第一次迭代之前只求值一次。对于 slice,range 之后使用的是那个 slice 头,所以它的长度是固定的:
追加的 99 永远不会被遍历到,因为循环的长度已经固定为 3。但 nums[2] = 30 能被看到,因为此时 nums 和正在遍历的 slice 仍然共享同一个底层数组。把这两行交换一下,30 也会消失:append 超出了容量 3,分配了一个新数组,写入就落到了新数组上。遍历一个数组值时会先复制整个数组,所以对 arr[2] 的修改看不到;遍历 &arr 或 arr[:] 可以避免复制。
遍历函数(Go 1.23)
从 Go 1.23 起,range 也接受迭代器函数。迭代器接收一个 yield 回调,每个值调用它一次,当 yield 返回 false 时停止(循环体执行 break 或 return 时就会如此):
iter.Seq[V] 和 iter.Seq2[K, V] 是这两种迭代器形态的标准名称。标准库中返回它们的有 slices.All、slices.Values、slices.Backward、maps.Keys、maps.Values、maps.All,以及 Go 1.24 中的 strings.SplitSeq、strings.Lines 和 bytes.SplitSeq。迭代器必须遵守 yield 的返回值:在 yield 返回 false 之后再次调用它会引发 panic。
循环变量每次迭代都是新的(Go 1.22)
从 Go 1.22 起,for ... range 声明的变量在每次迭代中都是新的。在闭包或 goroutine 中捕获 v,捕获的是这一次迭代的值,而不是一个最终保存着最后一个元素的共享变量。在 Go 1.22 之前,你需要在循环内写 v := v;现在这一行已经多余了。这条规则由 go.mod 中的 go 版本决定,所以旧模块会保持旧的行为。
常见错误
- 获取值变量的地址。 在 Go 1.22 之前,
ptrs = append(ptrs, &v)每次存入的都是同一个地址。从 1.22 起存入的是不同的地址,但每个都指向一个副本,而不是 slice 中的元素。要指向 slice 内部,使用&items[i]。 - 依赖 map 的顺序。 在小测试里看起来有序的输出,不会一直保持有序。
- 遍历一个没人关闭的 channel。 循环以及运行它的 goroutine 会永远等待下去。
- 按值遍历一个很大的数组。 整个数组会先被复制一遍。应该遍历它的一个 slice。
常见问题
Go 中的 range 返回什么?
取决于被遍历的对象。slice 或数组:索引和元素。字符串:字节索引和 rune。map:键和值。channel:每个接收到的值。整数 n(Go 1.22):0 到 n 减 1。迭代器函数(Go 1.23):函数 yield 出的任何值。你可以省略第二个变量,或者用 _ 丢弃第一个。
为什么修改 range 的值没有改变我的 slice?
值变量是元素的副本。for _, v := range items { v.Price = 0 } 只修改了副本。应该使用索引:for i := range items { items[i].Price = 0 },或者遍历一个指针 slice。
Go 中可以对整数使用 range 吗?
可以,从 Go 1.22 开始:for i := range 5 { ... } 让 i 从 0 取到 4。for range 5 { ... } 不使用变量,重复执行五次。模块的 go.mod 必须写明 go 1.22 或更高版本。
遍历 map 时删除元素安全吗?
安全。删除一个还没遍历到的条目,它就不会再出现;删除当前条目也没有问题。循环中添加条目同样是允许的,但新条目可能被遍历到,也可能不会,所以不要依赖这一点。