Go 流程控制
Go 的 if(含简单语句)、for 的多种形态、switch 默认不贯穿,以及 break/continue 与标签;强调早返回与扁平控制流。
[!info] 关联笔记
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)
}
建议运行:
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 {
// ...
}
规则:
- 条件不加圆括号(需要时用括号做运算优先级)
- 主体必须花括号
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(立即进入下一 case 体,不再检查下一 case 表达式) switch也可带 init:switch x := f(); x { ... }- 无 tag 的
switch { case cond: }适合多条件分支 - 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 j == 0 {
continue Outer // 进入外层下一轮
}
}
}
- 无标签时,
break/continue作用于最内层for/switch/select break也可带标签跳出指定的 for/switch/select- 标签在 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 有重要变化;写闭包捕获时必须查 range 语义。
4. 空 switch / 空 for
合法但要意图清晰;无限 for 必须有可靠退出(break、return、阻塞点)。
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 的交界,勿忙等。
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 习惯 |