Menu

Golang for 循环:四种写法详解与示例

Go 只有一个循环关键字 for,但它能胜任一切:三段式计数循环、只有条件的 while 式循环、无限循环,以及对切片、map、字符串、channel、整数和函数的 range 遍历。另外还有带标签的 break 和 continue,以及 Go 1.22 对循环变量的改动。

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

Go 只有一个循环关键字:for。它有四种形态,合起来涵盖了你在其他语言里用 for、while、do while 或 foreach 写的所有循环。

循环头外面不加括号,而花括号始终是必需的。

三段式循环

for init; condition; post {
	// body
}
  1. init 在循环开始前执行一次。它通常是一个短变量声明,比如 i := 0,这个变量只在循环内部存在。
  2. 每次迭代之前都会检查 condition。为 false 时,循环结束。
  3. 执行循环体。
  4. 每次迭代之后执行 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 或更高版本时,新行为才会生效。

Coddy 编程语言插图

和 Coddy 一起学编程

开始