跳到主要内容

Loop Control(循环控制)

Go 只使用 for 表达循环,但它既能写成计数循环,也能写成条件循环或无限循环。配合 rangebreakcontinue,可以覆盖大多数遍历与重复执行场景。

基本 for 循环

完整形式由初始化语句、条件表达式和后置语句组成,三部分之间使用分号分隔:

package main

import "fmt"

func main() {
for index := 0; index < 3; index++ {
fmt.Println(index)
}
}

输出如下:

0
1
2

初始化语句只执行一次;每轮循环开始前检查条件;每轮循环体结束后执行后置语句。初始化语句中声明的变量只在 for 语句内有效。

条件循环

省略初始化语句和后置语句后,for 的作用类似其他语言中的 while。Go 不提供 while 关键字:

package main

import "fmt"

func main() {
remaining := 3
for remaining > 0 {
fmt.Println(remaining)
remaining--
}
}

条件必须是布尔表达式,不能使用整数或字符串隐式判断真假。

无限循环

省略所有控制部分即可创建无限循环:

for {
// 持续处理任务,直到执行 break、return 或发生其他退出事件。
}

服务端程序经常使用这种形式持续接收任务。实际代码必须设计退出路径,例如检测 context.Context 的取消信号,避免循环无法停止或持续占用 CPU。

使用 range 遍历

range 可以遍历数组、切片、字符串、映射和通道。Go 1.22 起还可以对整数使用 range,Go 1.23 起可以遍历符合约定签名的迭代器函数;这些版本规则以 Go 语言规范的 range 条款 为准。本文先聚焦最常见的容器遍历。遍历切片时,每轮得到索引和对应元素值:

package main

import "fmt"

func main() {
names := []string{"Ada", "Linus", "Rob"}
for index, name := range names {
fmt.Printf("%d: %s\n", index, name)
}
}

不需要某个返回值时,使用空白标识符 _,或者直接省略后面的值:

for _, name := range names {
fmt.Println(name)
}

for index := range names {
fmt.Println(index)
}

遍历不同类型时,range 产生的值并不相同:

被遍历的值第一个值第二个值
数组、切片索引元素副本
字符串字节索引Unicode 码点对应的 rune
映射值的副本
通道接收到的值

range 得到的元素或映射值是赋值后的副本。修改循环变量不会直接修改原容器;需要修改切片元素时,应通过索引访问原切片:

for index := range names {
names[index] = "user: " + names[index]
}

映射的遍历顺序未作规定,不应依赖某次运行观察到的顺序。字符串遍历还涉及 UTF-8 解码,具体行为见 String(字符串)

continuebreak

continue 跳过当前轮剩余语句并进入下一轮,break 立即结束当前循环:

package main

import "fmt"

func main() {
for number := 1; number <= 10; number++ {
if number%2 != 0 {
continue
}
if number > 6 {
break
}
fmt.Println(number)
}
}

输出为 246。在嵌套循环中,不带标签的 breakcontinue 只作用于最内层循环。

使用标签控制外层循环

标签可以让 breakcontinue 明确作用于外层语句。它适合解决少量嵌套控制问题,但过多标签通常意味着代码应拆分为函数。

package main

import "fmt"

func main() {
matrix := [][]int{
{3, 7, 9},
{4, 8, 12},
}

Search:
for row, values := range matrix {
for column, value := range values {
if value == 8 {
fmt.Printf("found at %d,%d\n", row, column)
break Search
}
}
}
}

标签需要紧邻它所标记的语句。break 还可以结束 switchselect;带标签时,目标必须是包含该 break 的外层 forswitchselect

循环中的资源释放

defer 在外围函数返回时才执行,而不是在每轮循环结束时执行。直接在长循环中不断注册 defer,可能使文件、响应体等资源迟迟不能释放。可以把单轮工作提取为函数:

func process(path string) error {
file, err := os.Open(path)
if err != nil {
return err
}
defer file.Close()

// 处理当前文件。
return nil
}

for _, path := range paths {
if err := process(path); err != nil {
return err
}
}

这样每次 process 返回时都会及时执行对应的 defer。完整程序还需要导入 os,并在一个可返回 error 的函数中执行循环。

常见错误

  • 循环条件永远不变,导致死循环。
  • 在循环中向正在遍历的切片追加元素,却假设遍历次数会随之变化。
  • 修改 range 的值变量,误以为原容器中的元素也会改变。
  • 依赖映射的遍历顺序。
  • 在长循环中累计 defer,使资源延迟到外层函数返回时才释放。

与 TypeScript/JavaScript 的关键差异

TypeScript/JavaScript 提供 forwhiledo...whilefor...of 等多种语法;Go 用一种 for 语句覆盖这些场景。Go 的 range 也不等同于 JavaScript 的 for...infor...of:它针对被遍历类型产生规定的值组合,而且循环变量接收的是赋值结果。

通道遍历、关闭责任和取消退出见 Concurrency(并发)

小结

  • Go 的所有循环都使用 for
  • range 的返回值取决于被遍历类型,容器元素通常以副本形式赋给循环变量。
  • continue 跳过当前轮,break 结束循环,标签可以明确控制外层语句。
  • 不要依赖映射遍历顺序,也不要在长循环中无边界地累计资源和 defer