Go 控制流:if、for、switch
AI 辅助生成本文由 AI 起草并经人工核查,示例均标明软件版本;发现错误请以官方文档为准。
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 标志位清晰,但嵌套超过两层就该考虑抽成函数。
常见错误与注意事项
- 写
while或do-while:Go 没有这两个关键字,用for表达。 - 在
range里修改元素值:改的是副本,要按下标改。 - 依赖 map 遍历顺序:顺序随机,需要顺序就排序键。
- 以为字符串
range的下标是字符序号:是字节下标,中文会跳 3。 - 在
switch里期待 C 的穿透:默认已 break,需要显式fallthrough。 if里用非 bool 值:if 1编译失败,必须写比较表达式。
小结
if条件必须是 bool,可用初始化语句限制变量作用域。- 只有
for:三段式、条件式、无条件、range四种形态。 range给的是元素副本;map 遍历无序;string 遍历按字节下标 + rune。switch默认不穿透,可无表达式,也可做类型 switch。- 多层跳出用标签,比标志位清晰。