Go 只有一个循环关键字:for。它有四种形态,合起来涵盖了你在其他语言里用 for、while、do while 或 foreach 写的所有循环。
循环头外面不加括号,而花括号始终是必需的。
三段式循环
for init; condition; post {
// body
}
init在循环开始前执行一次。它通常是一个短变量声明,比如i := 0,这个变量只在循环内部存在。- 每次迭代之前都会检查
condition。为 false 时,循环结束。 - 执行循环体。
- 每次迭代之后执行
post,通常是i++。
在 Go 中 i++ 是语句而不是表达式,所以 post 里不能写 i++, j--。改用并行赋值:i, j = i+1, j-1。
三个部分都是可选的。for ; i < 10; {} 是合法的,gofmt 会把它改写成 for i < 10 {},也就是下一种形式。
只有条件的循环(Go 的 while)
只带一个条件时,for 的行为就和其他语言里的 while 一样:
Go 没有单独的 do while。while 循环一页介绍了如何实现“至少执行一次”的效果。
无限循环
for 后面什么都不写,就会一直循环下去。用 break、return、os.Exit 或 panic 退出:
for {
line, err := reader.ReadString('\n')
if err != nil {
break
}
process(line)
}
这是服务器、worker 和输入循环的标准结构。循环体为空的 for {} 会占满一个 CPU 核心;在真实代码中,循环体会阻塞在某个操作上(读取、从 channel 接收、select)。
range 循环
range 遍历一个集合,每次迭代产生一个或两个值。具体是什么值,取决于你遍历的对象:
| 遍历对象 | 第一个值 | 第二个值 |
|---|---|---|
| 切片或数组 | 索引 | 元素(副本) |
| 字符串 | 字节索引 | rune(Unicode 字符) |
| map | 键 | 值 |
| channel | 元素 | (无) |
整数 n(Go 1.22) | 0 到 n 减 1 | (无) |
| 迭代器函数(Go 1.23) | 函数产出的值 | 函数产出的值 |
输出体现了三个细节:
- 值变量是一个副本。在循环里写
v *= 2不会改变切片;要修改元素,就像第三个循环那样用nums[i]按索引访问切片。 - 字符串的索引是字节偏移量。
é占两个字节,所以索引从 1 跳到了 3。 - map 的顺序是刻意随机的。Go 把它随机化,让代码无法依赖它。
slices.Sorted(maps.Keys(m))(Go 1.23)可以按顺序给出所有键。
range 一页完整介绍了 range,包括 channel、迭代器函数和复制规则。
break 和 continue
break 结束最内层的循环。continue 跳到下一次迭代(在三段式循环中,会先执行 post):
带标签的 break 和 continue
普通的 break 只会离开最内层的循环。要从嵌套循环内部退出外层循环,就给外层循环加上标签:
标签是一个标识符加冒号,直接放在 for 前面。和未使用的变量一样,未使用的标签会被 Go 报告为编译错误。
switch 和 select 中的 break
在 for 里面,switch 或 select 的 case 中的 break 离开的是 switch 或 select,而不是循环:
for {
switch cmd := next(); cmd {
case "quit":
break // only exits the switch; the loop keeps running
}
}
要在 switch 或 select 内部离开循环,用带标签的 break 或者 return。
Go 1.22 起的循环变量
Go 1.22 改变了循环变量的作用域:现在每次迭代都会得到 i 和 v 的全新副本。以前整个循环复用同一个变量,这会让捕获它的闭包和 goroutine 出错:
在 Go 1.22 及以后的版本中,这段代码打印 0 1 2 和 [aa bb cc]。在旧语义下,用 for i := 0; i < 3; i++ 写的同一个程序(1.22 之前还不能对整数使用 range)会打印 3 3 3,而且 goroutine 会在共享的 i 和 name 上产生竞态。旧的变通写法,即在循环体开头写 i := i,现在已经不需要了。
这一行为由 go.mod 中的 go 行按模块决定,而不是由编译器版本决定。一个仍然写着 go 1.21 的模块,即使用 Go 1.24 构建,也会保留旧语义。
常见错误
修改的是副本。 for _, v := range items { v.Count++ } 改的是副本。用 items[i].Count++,或者遍历一个指针切片。
一边 range 一边追加。 range 只在开始时对切片求值一次。在循环内追加的元素不会被遍历到,所以循环仍然会结束,但看不到新加入的元素。
在正向循环中按索引从切片删除元素。 删除元素 i 会让后面的元素整体左移,于是下一个元素就被跳过了。可以构建一个新切片、倒序遍历,或者使用 slices.DeleteFunc。
用 <= 导致差一错误。 for i := 0; i <= len(s); i++ 在最后一轮会读取 s[len(s)],并以 index out of range panic。用 <,更好的做法是用 range。
在条件里做耗时的工作。 条件在每次迭代时都会执行。for i := 0; i < expensiveCount(); i++ 每次都会调用这个函数;在循环开始前计算一次即可。
用浮点数做计数器。 for x := 0.0; x != 1.0; x += 0.1 永远不会结束,因为 0.1 没有精确的二进制表示。用整数计数,再从它算出浮点数。
常见问题
在 Go 中怎样写 for 循环?
经典写法由三部分组成,用分号分隔,不加括号:
for i := 0; i < 5; i++ {
fmt.Println(i)
}
从 Go 1.22 开始,你还可以写 for i := range 5,它从 0 数到 4。要遍历切片,用 for i, v := range items。
Go 有 while 循环吗?
没有 while 关键字,但只带条件的 for 就是 while 循环:for n > 0 { n /= 2 }。不带条件的 for { ... } 是无限循环,用 break 或 return 退出。
在 Go 中怎样遍历 map?
for key, value := range m { ... }。遍历顺序没有规定,而且运行时会刻意随机化,所以每次循环的顺序都可能不同。千万不要依赖它。如果需要稳定的顺序,先收集所有键,用 slices.Sort 排序,再遍历排好序的键。
在 Go 中怎样跳出嵌套循环?
在外层循环前放一个标签,并在 break 中写出它的名字:outer: for ... { for ... { if found { break outer } } }。普通的 break 只会离开最内层的 for、switch 或 select。continue outer 同理,会跳到带标签循环的下一次迭代。
Go 1.22 对循环变量做了什么改动?
在 Go 1.22 之前,一个 for 循环只有一个变量,所有迭代共用它,所以在循环中启动的闭包和 goroutine 常常都只看到最后一个值。从 Go 1.22 开始,每次迭代都有自己的副本,所以在闭包中捕获 i 或 v 会符合预期。当模块的 go.mod 写的是 go 1.22 或更高版本时,新行为才会生效。