首页 / Go 语言入门教程 / 循环变量语义与跳转

Go 语言入门教程

循环变量语义与跳转

本教程共 80 篇 · 第 21 篇 · 更新于 2026-07-27 · 约 8 分钟阅读

GoGo 入门教程循环变量breakcontinuelabelgotoGo 1.22

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 fixgo vet 检查代码,移除不再需要的 i := i workaround。

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 不能:

  1. 跳过变量声明
  2. 跳入代码块内部
  3. 跳到外层代码块
// 报错:goto 跳过了变量声明
goto skip
    x := 10
skip:
    fmt.Println(x)

什么时候用 goto

实际上,goto 在 Go 里几乎不需要用。大部分场景可以用 forbreakcontinue、函数返回来替代。

唯一比较合理的场景是错误处理中的资源清理

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
            }
            // 处理正常值
        }
    }
}

常见问题

标签命名

标签命名遵循标识符规则。通常用描述性名称,如 outersearchrowLoop。不要用 l1l2 这种无意义的名字。

标签的作用域

标签只在声明它的函数内有效。不同函数可以有同名标签。

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 存在但基本不用

下一节进入函数部分,从函数声明和调用开始。