Go 控制流:if、for、switch

AI 辅助生成本文由 AI 起草并经人工核查,示例均标明软件版本;发现错误请以官方文档为准。

Go 1.24

Go 的控制流关键字少得惊人:没有 while、没有 do-while、switch 默认不穿透。少即是多——但要先弄清它到底怎么工作。

if:条件不需要括号,支持初始化语句

if score >= 60 {
    fmt.Println("及格")
} else if score >= 40 {
    fmt.Println("补考")
} else {
    fmt.Println("不及格")
}

条件必须是 bool,不能写 if score 或 if 1,这与其他语言最大的差别之一。

if 可以带一条初始化语句,变量作用域限制在 if-else 内:

if n, err := strconv.Atoi(input); err != nil {
    fmt.Println("转换失败:", err)
} else {
    fmt.Println("结果是", n)
}
// 这里访问 n 会编译错误

这个写法在「先取值、再判断错误」时非常常用,能避免变量泄漏到函数作用域。

for:唯一的循环

Go 只有 for 一个循环关键字,但要什么形式都写得出来。

三段式(最接近 C 的写法):

for i := 0; i < 3; i++ {
    fmt.Println(i)
}

只有条件(相当于 while):

n := 1
for n < 100 {
    n *= 2
}

无条件(相当于 while (true),需要自行退出):

for {
    line, err := reader.ReadString('\n')
    if err != nil {
        break
    }
    fmt.Print(line)
}

range 遍历:

nums := []int{10, 20, 30}

for i, v := range nums {
    fmt.Println(i, v)
}

for _, v := range nums {   // 只要值
    fmt.Println(v)
}

for i := range nums {      // 只要下标
    fmt.Println(i)
}

range 遍历时取得的是元素的副本,修改 v 不会改变切片:

for _, v := range nums {
    v *= 2          // 无效,nums 不变
}

for i := range nums {
    nums[i] *= 2    // 正确,按索引修改
}

range 与不同数据结构

被遍历对象 第一个返回值 第二个返回值
切片 / 数组 下标 元素副本
string 字节下标 rune
map 键 值
channel — 接收到的值

遍历 map 时顺序是随机的,Go 故意如此,防止依赖顺序的代码悄悄上线:

m := map[string]int{"a": 1, "b": 2, "c": 3}
for k, v := range m {
    fmt.Println(k, v)   // 每次运行顺序可能不同
}

需要稳定顺序时先把键取出来排序:

keys := make([]string, 0, len(m))
for k := range m {
    keys = append(keys, k)
}
sort.Strings(keys)

for _, k := range keys {
    fmt.Println(k, m[k])
}

遍历字符串时下标是字节位置,不是字符序号:

for i, r := range "a你b" {
    fmt.Printf("%d %c\n", i, r)
}
// 0 a
// 1 你
// 4 b   ("你" 占 3 字节)

switch:默认不穿透、可无表达式

switch status {
case "pending", "running":
    fmt.Println("进行中")
case "done":
    fmt.Println("已完成")
default:
    fmt.Println("未知")
}

与 C 的关键差异:

  • 每个 case 结束自动 break,不需要写,也不能靠忘写来穿透。
  • 想穿透必须显式写 fallthrough(实际很少用)。
  • case 可以是逗号分隔的多值。
  • switch 后面可以不写表达式,此时等价于 switch true:
switch {
case score >= 90:
    fmt.Println("优秀")
case score >= 60:
    fmt.Println("及格")
default:
    fmt.Println("不及格")
}

switch 也能带初始化语句:

switch n := len(items); {
case n == 0:
    fmt.Println("空")
case n < 10:
    fmt.Println("少量")
default:
    fmt.Println("很多")
}

类型 switch 与类型断言

处理 any(任意类型)时用类型 switch:

func describe(v any) string {
    switch x := v.(type) {
    case int:
        return fmt.Sprintf("整数 %d", x)
    case string:
        return "字符串 " + x
    case nil:
        return "nil"
    default:
        return fmt.Sprintf("其他类型 %T", x)
    }
}

注意 case nil 判断的是接口本身为 nil,与接口内包着 nil 指针的情况不同(接口细节见本系列最后一篇)。

break、continue 与标签

for _, row := range matrix {
    for _, cell := range row {
        if cell < 0 {
            continue outer   // 跳过外层本次迭代
        }
        if cell == 0 {
            break outer      // 结束外层循环
        }
    }
}

上面的写法需要先给外层循环打标签:

outer:
for _, row := range matrix {
    // ...
}

标签比维护一个 found := false 标志位清晰,但嵌套超过两层就该考虑抽成函数。

常见错误与注意事项

  1. 写 while 或 do-while:Go 没有这两个关键字,用 for 表达。
  2. 在 range 里修改元素值:改的是副本,要按下标改。
  3. 依赖 map 遍历顺序:顺序随机,需要顺序就排序键。
  4. 以为字符串 range 的下标是字符序号:是字节下标,中文会跳 3。
  5. 在 switch 里期待 C 的穿透:默认已 break,需要显式 fallthrough。
  6. if 里用非 bool 值:if 1 编译失败,必须写比较表达式。

小结

  • if 条件必须是 bool,可用初始化语句限制变量作用域。
  • 只有 for:三段式、条件式、无条件、range 四种形态。
  • range 给的是元素副本;map 遍历无序;string 遍历按字节下标 + rune。
  • switch 默认不穿透,可无表达式,也可做类型 switch。
  • 多层跳出用标签,比标志位清晰。

下一步