循环变量语义与跳转
本教程共 80 篇 · 第 21 篇 · 更新于 2026-07-27 · 约 8 分钟阅读
21. 循环变量语义与跳转
本节目标:理解 Go 1.22 循环变量语义变更的原因和影响,掌握 break/continue 配合标签跳转多层循环,了解 goto 的使用场景。
Go 1.22 循环变量变更
旧语义(1.21 及之前)
在 Go 1.22 之前,for 循环的变量只创建一次,每次迭代复用同一个变量:
// Go 1.21 的行为(伪代码说明)
var i int // 只创建一次
for i = 0; i < 3; i++ {
// 每次迭代用的是同一个 i
}
这会导致一个经典的陷阱:在循环里启动 goroutine 时,所有 goroutine 拿到的都是同一个变量的最终值。
// Go 1.21:输出 3 3 3(不是 0 1 2)
for i := 0; i < 3; i++ {
go func() {
time.Sleep(time.Millisecond)
fmt.Println(i) // 全部打印 3
}()
}
因为在 goroutine 真正执行时,循环已经结束了,i 的值是 3。
旧代码的解决办法是每次迭代创建一个局部副本:
for i := 0; i < 3; i++ {
i := i // 创建新的局部变量
go func() {
fmt.Println(i) // 0 1 2
}()
}
或者把 i 作为参数传入:
for i := 0; i < 3; i++ {
go func(i int) {
fmt.Println(i)
}(i)
}
新语义(1.22 起)
Go 1.22 开始,每次迭代都创建新的变量:
// Go 1.22+:输出 0 1 2
for i := 0; i < 3; i++ {
go func() {
time.Sleep(time.Millisecond)
fmt.Println(i) // 每次迭代 i 是不同的变量
}()
}
每次迭代的 i 是一个全新的变量,goroutine 捕获的是各自的 i,不再共享。
Note这个变更消除了 Go 最常见的 bug 之一。从 1.22 开始,你不需要再写
i := i这种 workaround 了。
for-range 同样适用
// Go 1.22+ 每次迭代的 v 是新变量
for _, v := range items {
go func() {
process(v) // 每个 goroutine 处理不同的值
}()
}
对闭包的影响
不仅 goroutine,所有在循环里创建的闭包都受益:
// Go 1.21:所有函数返回 3
var funcs []func() int
for i := 0; i < 3; i++ {
funcs = append(funcs, func() int { return i })
}
for _, f := range funcs {
fmt.Println(f()) // 3 3 3
}
// Go 1.22+:函数返回各自的值
var funcs []func() int
for i := 0; i < 3; i++ {
funcs = append(funcs, func() int { return i })
}
for _, f := range funcs {
fmt.Println(f()) // 0 1 2
}
向后兼容
这个变更不破坏现有代码。如果你的代码在 1.21 之前写了 i := i 这种 workaround,在 1.22+ 上运行也没问题,只是多了一步不必要的操作。
Tip升级到 Go 1.22+ 后,可以用
go fix或go vet检查代码,移除不再需要的i := iworkaround。
break 跳出循环
break 用于跳出当前循环:
for i := 0; i < 10; i++ {
if i == 5 {
break
}
fmt.Println(i)
}
// 输出:0 1 2 3 4
break 只跳出最内层
嵌套循环中,break 只跳出最内层:
for i := 0; i < 3; i++ {
for j := 0; j < 3; j++ {
if j == 1 {
break // 只跳出内层 j 循环
}
fmt.Printf("i=%d j=%d\n", i, j)
}
}
// 输出:
// i=0 j=0
// i=1 j=0
// i=2 j=0
用标签跳出多层循环
要跳出多层循环,用标签(Label):
outer:
for i := 0; i < 3; i++ {
for j := 0; j < 3; j++ {
if i == 1 && j == 1 {
break outer // 跳出 outer 标签标记的循环
}
fmt.Printf("i=%d j=%d\n", i, j)
}
}
// 输出:
// i=0 j=0
// i=0 j=1
// i=0 j=2
// i=1 j=0
标签定义在 for 语句前面,用标识符加冒号。break outer 会跳出 outer 标记的那层循环。
continue 跳过本次
continue 跳过当前迭代,进入下一次:
for i := 0; i < 10; i++ {
if i%2 == 0 {
continue // 跳过偶数
}
fmt.Println(i)
}
// 输出:1 3 5 7 9
continue 配合标签
continue 也可以配合标签,跳到外层循环的下一次迭代:
outer:
for i := 0; i < 3; i++ {
for j := 0; j < 3; j++ {
if j == 1 {
continue outer // 跳过外层循环的剩余部分,进入下一次 i
}
fmt.Printf("i=%d j=%d\n", i, j)
}
}
// 输出:
// i=0 j=0
// i=1 j=0
// i=2 j=0
continue outer 不是重新开始外层循环,而是跳到外层循环的 post 语句(i++),然后继续下一次外层迭代。
goto 语句
Go 保留了 goto,但很少用。goto 可以跳到同一函数内的任意标签:
func main() {
i := 0
loop:
fmt.Println(i)
i++
if i < 5 {
goto loop
}
}
// 输出:0 1 2 3 4
goto 的限制
goto 不能:
- 跳过变量声明
- 跳入代码块内部
- 跳到外层代码块
// 报错:goto 跳过了变量声明
goto skip
x := 10
skip:
fmt.Println(x)
什么时候用 goto
实际上,goto 在 Go 里几乎不需要用。大部分场景可以用 for、break、continue、函数返回来替代。
唯一比较合理的场景是错误处理中的资源清理:
func process() error {
f1, err := os.Open("file1")
if err != nil {
goto cleanup
}
defer f1.Close()
f2, err := os.Open("file2")
if err != nil {
goto cleanup
}
defer f2.Close()
// 正常处理...
cleanup:
// 统一清理逻辑
return err
}
不过现代 Go 更推荐用 defer 处理清理,goto 基本不用。
Warning不要滥用
goto。它会让代码难以理解和维护。99% 的情况下你有更好的选择。
标签的实际应用
查找二维数组
func find(matrix [][]int, target int) bool {
found := false
search:
for i, row := range matrix {
for j, val := range row {
if val == target {
fmt.Printf("找到 %d 在 [%d][%d]\n", target, i, j)
found = true
break search
}
}
}
return found
}
提前退出多层循环
func processGrid(grid [][]int) {
for i := range grid {
rowLoop:
for j := range grid[i] {
if grid[i][j] < 0 {
fmt.Println("发现负数,停止处理")
break rowLoop
}
// 处理正常值
}
}
}
常见问题
标签命名
标签命名遵循标识符规则。通常用描述性名称,如 outer、search、rowLoop。不要用 l1、l2 这种无意义的名字。
标签的作用域
标签只在声明它的函数内有效。不同函数可以有同名标签。
goto vs break/continue
| 需求 | 推荐方式 |
|---|---|
| 跳出当前循环 | break |
| 跳出多层循环 | break label |
| 跳过当前迭代 | continue |
| 跳到外层循环下一次 | continue label |
| 跳到函数内某处 | goto(但不推荐) |
循环变量在循环外能用吗
for i := 0; i < 5; i++ {
// ...
}
fmt.Println(i) // 报错:i 在这里不可见
循环变量的作用域只在循环体内。循环外访问不到。
学完这节你能做什么
- 理解 Go 1.22 循环变量语义变更的原因和影响
- 不再需要写
i := i的 workaround - 用
break label跳出多层循环 - 用
continue label跳到外层循环下一次 - 知道
goto存在但基本不用
下一节进入函数部分,从函数声明和调用开始。