Go 流程控制
Go 的 if(含简单语句)、for 的多种形态、switch 默认不贯穿,以及 break/continue 与标签;强调早返回与扁平控制流。
[!info] related notes
Go 流程控制
这个概念为什么会出现
任何非玩具程序都要表达:
- 条件成立时走哪条路
- 重复做某事直到条件变化
- 按离散情况分支
- 从多层循环中跳出
Go 有意保持控制结构种类少、规则硬:没有 while/do-while,循环统一为 for;switch 默认不 fallthrough;条件外无需括号。这降低了方言分叉,也让错误处理里的“早返回”成为自然风格。
[!abstract] 一句话理解 Go 用
if/for/switch表达几乎所有同步控制流:if/switch可带简单初始化语句,for一种关键字覆盖三种循环,switch默认不贯穿,标签配合break/continue可跳出外层。
最小可运行示例
先把示例放进业务场景,再看代码:
场景:成绩单服务的评级与批处理
示例演示四件事:
- 单科评级:分数 → A/B/C,非法分数直接拒绝
- 重试计数循环:某种同步任务最多跑 3 次(
for三要素) - 倒计时/while 形态:队列里还有任务就继续消费(
for condition) - 汇总:把本页三条分数加总(
for range)
Go 没有 while,全靠 for 的不同写法;switch 默认不 fallthrough,适合评级这种互斥区间。
package main
import "fmt"
// letterGrade 模拟“百分制转等级”。
// 业务意图:非法输入早返回;合法输入用清晰区间分支。
// 教学点:switch 无表达式 ≈ if-else 链;case 自动结束,不必写 break。
func letterGrade(score int) string {
if score < 0 || score > 100 {
return "invalid"
}
switch {
case score >= 90:
return "A"
case score >= 80:
return "B"
default:
return "C-or-below"
}
}
func main() {
// if 短语句:g 的作用域仅限 if/else,不污染后面代码
if g := letterGrade(95); g != "invalid" {
fmt.Println("grade:", g) // A
}
// for 三要素:等价“重试/页码”类计数循环
for i := 0; i < 3; i++ {
fmt.Println("attempt", i)
}
// for 条件:等价其他语言 while —— 还有剩余任务就继续
left := 3
for left > 0 {
fmt.Println("jobs left", left)
left--
}
// for range:汇总本页分数
sum := 0
for _, v := range []int{1, 2, 3} {
sum += v
}
fmt.Println("page sum", sum)
}
保存为 main.go,在已执行 go mod init 的模块目录运行:
go run .
期望输出:
grade: A
attempt 0
attempt 1
attempt 2
jobs left 3
jobs left 2
jobs left 1
page sum 6
结合场景再看三个关注点
-
if短语句限制作用域
g := letterGrade(...)只在分支里可见,避免临时变量漏到函数后半段。 -
只有
for,三种循环形态
计数、while 条件、range 遍历,关键字都是for。 -
switch默认不贯穿
命中>=90不会掉进>=80;需要贯穿时才显式fallthrough。
核心概念与准确模型
if:条件与简单语句
if init; condition {
// ...
} else if condition2 {
// ...
} else {
// ...
}
规则:
- 条件不要求外层圆括号,但 if (ok) 合法;括号也可控制表达式优先级\
- 主体必须花括号
init中声明的变量作用域覆盖if、else if、else- 条件必须是布尔类型(含以 bool 为底层的定义类型),无“非零即真”
与错误处理强绑定的形态:
if err := work(); err != nil {
return err
}
for:三种形态,一个关键字
// 1) 三段式(类 C)
for init; condition; post {
}
// 2) 条件循环(类 while)
for condition {
}
// 3) 无限循环
for {
break
}
range 形式:
for i, v := range coll {
}
for i := range coll {
}
for range coll { // 只要次数/遍历副作用
}
range 的复制、变量语义、map 迭代顺序等见 range 语义。本篇只把它当作 for 的一种形态。
switch:默认不贯穿
switch tag {
case a, b: // 多值
// ...
case c:
// ...
default:
// ...
}
关键规则:
- case 末尾默认 break,不会掉进下一 case
- 需要贯穿时必须显式
fallthrough(位于非最后分支的末尾,进入下一分支体且不检查其表达式;type switch 不允许)\ switch也可带 init:switch x := f(); x { ... }- 无 tag 的
switch { case cond: }适合多条件分支 - case 可以是常量或运行时表达式,须与 switch 表达式类型兼容;依次比较,匹配后不再求值后续 case\
类型相关的分支使用 type switch,语法为 switch x := v.(type),详见 类型断言与 type switch,不要与值 switch 混为一谈。
break / continue 与标签
Outer:
for i := 0; i < 3; i++ {
for j := 0; j < 3; j++ {
if i == 1 && j == 1 {
break Outer // 跳出外层 for
}
if i == 0 && j == 0 {
continue Outer // 进入外层下一轮
}
}
}
- 无标签 break 结束最内层 for/switch/select;continue 只继续最内层 for,即使它写在 switch/select 内\
- break 标签必须指向包围它的 for/switch/select;continue 标签只能指向包围它的 for\
- 标签在 Go 中少用;优先考虑拆函数、早返回、
goto更少用
goto(仅作边界了解)
Go 的 goto 只能跳到同一函数内的标签,不能跳入新的块或越过变量声明使其凭空进入作用域;结构化控制流通常更易读。
与 defer 的关系
defer 注册的调用在函数返回前执行,常与错误路径、资源释放交织。它改变的是退出路径上的调用顺序,不是新的分支语法。见 defer/panic/recover。
设计动机
- 少即是多
一种循环关键字,减少while/do变体。 - switch 更安全
默认不 fallthrough,避免 C 系经典 bug。 - 简单语句作用域
if err := ...; err != nil把临时变量关在分支里。 - 服务错误处理风格
扁平、早返回、显式分支,而不是表达式里塞复杂逻辑。
边界情况与反直觉行为
1. switch 的 fallthrough 很“硬”
fallthrough 不判断下一个 case 条件,直接执行下一 case 体。
2. break 在 switch 内
在 for { switch { case: break } } 中,无标签 break 只离开 switch,不离开 for。
3. range 变量语义随版本
采用 Go 1.22 及之后语言语义时,循环中声明的变量每轮独立;用 = 复用已有变量不变。语言版本也受模块 go 指令等控制,不能仅看安装的工具链版本;详见 range 语义。
4. 空 switch / 空 for
合法但要意图清晰;服务型无限 for 可以长期运行,但应按生命周期设计退出与等待;阻塞点减少忙等,不等于退出机制。
5. case 中声明
case 里用 := 时注意作用域与阴影,保持短小。
常见误区
[!warning] 常见误区:以为 switch 会像 C 一样自动贯穿 错误:写多个 case 指望掉下去。
正确:默认独立;需要时显式fallthrough或合并case a, b:。
[!warning] 常见误区:用 while 关键字 错误:找
while。
正确:for condition { }。
[!warning] 常见误区:深层嵌套代替早返回 错误:
if ok { if err==nil { if ... } }。
正确:失败条件先return/continue,主路径留在低缩进。
[!warning] 常见误区:break 期望跳出外层 for 错误:在内层 switch/for 写
break以为结束全部循环。
正确:标签break Outer或重构函数。
[!warning] 常见误区:把 type switch 当普通 switch 学完即止 错误:忽略
.(type)形态。
正确:接口动态类型分支见专题笔记。
与相邻概念对比
| 概念 | 差异 |
|---|---|
| 运算符/表达式 | 产出值;控制流选择语句路径 |
| 函数返回 | 最强的“跳出”与错误传播手段 |
| select | 在 channel 通信上等待,不是值 switch |
| type switch | 按接口动态类型分支 |
| defer | 绑定函数退出,不替代 if/for |
工程实践
- 错误路径早返回
与 错误处理 一致,保持主逻辑直读。 - 条件保持纯粹
复杂条件提取为函数或布尔变量命名。 - 循环边界写清楚
优先range遍历集合;需要索引写入时再下标循环。 - switch 优于长 else-if
离散状态、命令字、HTTP 方法等用 switch。 - 标签克制
出现多层 break 标签时,考虑把内层提成函数。 - 测试分支
表驱动覆盖每个 case/default 与循环终止条件。 - 并发退出
循环中检查ctx.Done()属于控制流与 context 的交界,勿忙等。
以下为分发片段,start、stop 与 fmt 导入由应用提供:
func handle(cmd string) error {
switch cmd {
case "start":
return start()
case "stop":
return stop()
default:
return fmt.Errorf("unknown cmd %q", cmd)
}
}
可验证实验
实验 1:if 简单语句作用域
在 if x := 1; x > 0 { } 外打印 x,确认编译失败。
实验 2:for 三种形态
分别用三段式、条件式、无限 + break 打印 0..2。
实验 3:switch 不贯穿
连续两个 case 各打印一行,验证只执行匹配的一个;再加 fallthrough 对比。
实验 4:标签 break
双层循环,内层 break Outer 后观察外层是否继续。
实验 5:switch 内 break 与 for
for { switch { case true: break } } 是否终止 for?如何改成终止 for?
本节总结
- 本质:少量语句覆盖条件、循环、多路分支。
- 关键规则:if/switch 可 init;for 统一循环;switch 默认不贯穿;标签增强 break/continue。
- 最易错:fallthrough 预期、break 层次、滥用嵌套、忽略 type switch 专题。
- 下一步:函数、错误处理、range、type switch。
自测题
概念题
- 为什么 Go 没有
while?如何写等价循环? switch默认是否 fallthrough?如何显式贯穿?if err := f(); err != nil里err的作用域到哪?
代码推理题
for i := 0; i < 2; i++ {
switch i {
case 0:
break
case 1:
fmt.Println("one")
}
fmt.Println("i", i)
}
会打印什么?若要在 i==0 时直接结束 for,怎么改?
工程思考题
处理嵌套 JSON 结构时写了三层 for + switch,内层多处 break 行为不符合预期。优先重构策略是什么?
参考答案
展开
- 设计上合并循环形态;
for condition { }即 while。 - 默认不贯穿;使用
fallthrough语句。 - 覆盖整个 if-else 链,出链不可见。
代码题:break只离开 switch,仍打印i 0、one、i 1。要结束 for:标签break Loop,或return,或把条件提到 for。
工程题:内层提函数并用返回值表达“继续/停止/错误”;少用标签;表驱动分发替换深 switch。
延伸阅读与资料来源
| 资料 | 类型 | 支撑 |
|---|---|---|
| Spec — Statements | 规范 | if/for/switch/break 等 |
| Spec — If statements | 规范 | 简单语句与作用域 |
| Spec — For statements | 规范 | for / range |
| Spec — Switch statements | 规范 | 表达式 switch 与 fallthrough |
| Tour — Flow control | 教程 | 入门练习 |
| Effective Go — Control structures | 官方文档 | 风格与 if/switch 习惯 |