Loop Control(循环控制)
Go 只使用 for 表达循环,但它既能写成计数循环,也能写成条件循环或无限循环。配合 range、break 和 continue,可以覆盖大多数遍历与重复执行场景。
基本 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(字符串)。
continue 与 break
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)
}
}
输出为 2、4、6。在嵌套循环中,不带标签的 break 和 continue 只作用于最内层循环。
使用标签控制外层循环
标签可以让 break 或 continue 明确作用于外层语句。它适合解决少量嵌套控制问题,但过多标签通常意味着代码应拆分为函数。
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 还可以结束 switch 或 select;带标签时,目标必须是包含该 break 的外层 for、switch 或 select。
循环中的资源释放
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 提供 for、while、do...while、for...of 等多种语法;Go 用一种 for 语句覆盖这些场景。Go 的 range 也不等同于 JavaScript 的 for...in 或 for...of:它针对被遍历类型产生规定的值组合,而且循环变量接收的是赋值结果。
通道遍历、关闭责任和取消退出见 Concurrency(并发)。
小结
- Go 的所有循环都使用
for。 range的返回值取决于被遍历类型,容器元素通常以副本形式赋给循环变量。continue跳过当前轮,break结束循环,标签可以明确控制外层语句。- 不要依赖映射遍历顺序,也不要在长循环中无边界地累计资源和
defer。