Menu

Go range 用法:各种形式、返回值与常见陷阱

for range 在遍历 slice、数组、字符串、map、channel、整数和迭代器函数时分别返回什么,为什么值变量是一个副本,在遍历时修改 slice 或 map 会发生什么,以及 Go 1.23 中的 range over func。

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

range 是 for 的一种形式,用来遍历一个集合。每次迭代它给你一个或两个值,具体是什么值取决于你遍历的类型:

遍历对象第一个值第二个值说明
slice []T、数组 [n]T索引 int元素 T,是副本
数组指针 *[n]T索引元素
字符串字节索引 intrune按 UTF-8 解码
map map[K]V键 K值 V顺序随机
channel chan T元素 T无直到 channel 关闭
整数 n0 到 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)KVGo 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 时删除元素安全吗?

安全。删除一个还没遍历到的条目,它就不会再出现;删除当前条目也没有问题。循环中添加条目同样是允许的,但新条目可能被遍历到,也可能不会,所以不要依赖这一点。

Coddy 编程语言插图

和 Coddy 一起学编程

开始