流程语句
# 第 6 章 流程语句
Go 的流程控制:
if-init、三种for、switch自带 break、goto与标签、Go 1.22 新增for i := range N、循环变量每轮新建。 关键词:if、for、switch、break/continue标签、goto、fallthrough、循环变量
# 目录介绍
- 6.1 本章学习目标
- 6.2 if 与 if-init
- 6.3 for 三种形态
- 6.4 switch
- 6.5 break / continue 与标签
- 6.6 goto:什么时候才该用
- 6.7 没有 while / do-while / 三元
- 6.8 综合示例:FizzBuzz 与状态机
- 6.9 Go 新手陷阱 Top 5
- 6.10 思考题
- 6.11 推荐阅读
# 6.1 本章学习目标
- ✅ 默写
for的 5 种形态(三段式、单条件、无条件、range、range N) - ✅ 理解 Go 1.22 循环变量每轮新建的语义改动以及为什么"破坏 Go 1 兼容承诺"也要改
- ✅ 能用
if init; cond一行收窄变量作用域 - ✅ 区分 Go 的
switch与 C 的差异(默认不穿透、case 可任意类型、可裸 switch 替代 if-else) - ✅ 用带标签的
break/continue控制嵌套循环 - ✅ 理解为什么 Go 没有
while/do-while/ 三元运算符
# 6.2 if 与 if-init
# 6.2.1 没有括号的 condition
if x > 0 {
fmt.Println("positive")
} else if x < 0 {
fmt.Println("negative")
} else {
fmt.Println("zero")
}
2
3
4
5
6
7
强制规则:
| 规则 | 说明 |
|---|---|
| condition 不要括号 | if (x > 0) {} 多余括号会被 gofmt 保留但 lint 警告 |
{ 必须与 if 同行 | if x > 0\n{ 编译错(Go 的 ASI 自动分号机制不允许) |
| condition 必须是 bool | if 1 {}、if x = 0 {} 都编译错 |
else 必须紧跟 } 同行 | } \n else { } 编译错 |
最后一条是 Go 比较"独裁"的地方——它直接在语法层杜绝了"if/else 中间塞代码"的奇怪写法。
# 6.2.2 if err := f(); err != nil 模式
Go 的 if 支持"初始化语句 + 条件"两段式:
if 初始化语句; 条件 {
// ...
}
2
3
这是 Go 最有标志性的工程惯用法——错误处理的"短作用域"模式:
// ❌ 不推荐:err 泄露到外层作用域
err := doSomething()
if err != nil {
return err
}
// 这里还能看到 err,容易被后续代码"复用"出 bug
// ✅ 推荐:err 只在 if 内部可见
if err := doSomething(); err != nil {
return err
}
// 这里 err 已经不存在
2
3
4
5
6
7
8
9
10
11
12
类似地用于 map / 类型断言 / channel 接收:
if v, ok := m["key"]; ok {
fmt.Println(v)
}
if u, ok := i.(*User); ok {
fmt.Println(u.Name)
}
if msg, ok := <-ch; ok {
fmt.Println(msg)
}
2
3
4
5
6
7
8
9
10
11
# 6.2.3 init 子句的作用域
init 子句声明的变量在整个 if-else 链里都可见:
if v := compute(); v > 100 {
fmt.Println("big:", v)
} else if v > 10 {
fmt.Println("medium:", v) // ✅ 还看得到 v
} else {
fmt.Println("small:", v) // ✅ 也看得到
}
// fmt.Println(v) // ❌ 这里看不到了
2
3
4
5
6
7
8
这种"块级作用域 + 自动失效"是 Go 控制流的核心设计,很多 C / Java 程序员一开始用不惯,写久了反而会觉得"特别舒服"——因为出 bug 的概率断崖式下降。
# 6.3 for 三种形态
Go 把 for、while、do-while、foreach 全部统一成一个关键字 for——从五种形态。
# 6.3.1 三段式 for i := 0; i < n; i++
for i := 0; i < 10; i++ {
fmt.Println(i)
}
2
3
跟 C 一致,三个分号分隔的子句(init、cond、post)都可省略。
# 6.3.2 单条件 for cond 等价 while
i := 0
for i < 10 { // 等价于 while (i < 10)
fmt.Println(i)
i++
}
2
3
4
5
# 6.3.3 无条件 for {} 等价 while(true)
for {
msg, ok := <-ch
if !ok {
break
}
handle(msg)
}
2
3
4
5
6
7
事件循环、worker pool、长连接处理几乎全用这种形态。
# 6.3.4 for ... range 遍历
range 能遍历的 5 种类型:
| 类型 | 形式 | 第一返回值 | 第二返回值 |
|---|---|---|---|
| array / slice | for i, v := range s | 索引 | 元素拷贝 |
| string | for i, r := range s | 字节偏移(不是字符序号) | rune(Unicode 码点) |
| map | for k, v := range m | key | value |
| channel | for v := range ch | 接收到的值 | (无) |
| 整数(Go 1.22+) | for i := range N | 0..N-1 | (无) |
// slice:可以省略不需要的返回值
for _, v := range []int{10, 20, 30} {
fmt.Println(v)
}
// 只要索引
for i := range nums { ... }
// channel 自动检测关闭:sender close(ch) 后 range 自然退出
for msg := range ch {
handle(msg)
}
2
3
4
5
6
7
8
9
10
11
12
⚠️ 遍历的是副本:
users := []User{{Name: "A"}, {Name: "B"}} for _, u := range users { u.Name = "X" // ❌ 改的是 u 的副本,原 slice 不变 }1
2
3
4修改原元素请用索引:
users[i].Name = "X"。
# 6.3.5 Go 1.22:for i := range N 整数遍历
// Go 1.22+
for i := range 5 {
fmt.Println(i) // 0, 1, 2, 3, 4
}
// 等价于
for i := 0; i < 5; i++ { ... }
2
3
4
5
6
7
这是个纯语法糖——只为消灭 for i := 0; i < N; i++ 的样板代码。N 必须是非负整数,负数 / 浮点 / nil 都编译错。
# 6.3.6 Go 1.22:循环变量每轮新建
这是 Go 1.22 最影响日常代码的语义改动。看这段代码:
funcs := []func(){}
for _, v := range []int{1, 2, 3} {
funcs = append(funcs, func() { fmt.Println(v) })
}
for _, f := range funcs { f() }
2
3
4
5
Go 1.21 及之前:循环变量 v 是整个 for 共享一个变量,闭包捕获到的是同一个地址,迭代结束 v=3:
3
3
3
2
3
Go 1.22 起:每轮迭代 v 是新变量,闭包捕获到不同地址:
1
2
3
2
3
这是 Go 1.0 兼容承诺 (opens new window) 的一次罕见突破——官方判断"老语义带来的 bug 远多于偶尔依赖共享变量的代码",所以值得破坏向后兼容。
怎么知道当前模块用的是新语义? 看 go.mod 顶部的 go 1.22 或更高:
module example.com/foo
go 1.22 // ← 1.22+ 启用新循环变量语义
2
低于 1.22 的模块继续用老语义。混合使用规则详见 Go FAQ: Loop variable change (opens new window)。
老代码中的兼容写法——v := v 显式 shadow 仍然有效:
for _, v := range items {
v := v // 老 Go 强制每轮独立,新 Go 多余但无害
go func() { use(v) }()
}
2
3
4
# 6.3.7 Go 1.23:for range func 迭代器
Go 1.23 引入"range over func"——支持自定义迭代器:
// 一个迭代器:按顺序产出 [start, end)
func Range(start, end int) func(yield func(int) bool) {
return func(yield func(int) bool) {
for i := start; i < end; i++ {
if !yield(i) {
return
}
}
}
}
func main() {
for i := range Range(10, 13) {
fmt.Println(i) // 10, 11, 12
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
迭代器函数的签名共三种:
func(yield func() bool) // Seq0:无值
func(yield func(V) bool) // Seq[V]:单值
func(yield func(K, V) bool) // Seq2[K, V]:双值
2
3
yield 返回 false 表示"消费方提前 break,迭代器应停止"。标准库 iter、slices.All、maps.Keys 全部基于这套机制。
➡ 卷三第 12 章 迭代器与 range func 详讲。
# 6.4 switch
# 6.4.1 自带 break,不需要写
switch day {
case 1:
fmt.Println("Monday")
case 2:
fmt.Println("Tuesday")
default:
fmt.Println("Other")
}
2
3
4
5
6
7
8
与 C 最大的差别:每个 case 默认隐式 break,不会穿透到下一个 case。这是 Go 设计组觉得"C 的默认穿透是错的",直接反过来。
# 6.4.2 fallthrough 显式穿透
如果你确实想穿透,写 fallthrough:
switch x {
case 1:
fmt.Println("one")
fallthrough // 直接进入下一个 case 主体(不再判断 case 2 的条件)
case 2:
fmt.Println("two")
case 3:
fmt.Println("three")
}
// x=1 输出:one、two
2
3
4
5
6
7
8
9
10
注意 fallthrough 是"无条件跳到下一 case 主体"——不重新判断条件。这与某些教材的描述容易混淆。
工程上 fallthrough 极少用。多数想穿透的场景都能用"多值 case"代替。
# 6.4.3 多值 case 与表达式 case
// 多值 case:逗号分隔
switch day {
case 6, 7:
fmt.Println("Weekend")
case 1, 2, 3, 4, 5:
fmt.Println("Weekday")
}
// case 可以是任意表达式(不限于常量)
switch {
case x < 0:
fmt.Println("negative")
case x == 0:
fmt.Println("zero")
case x > 0:
fmt.Println("positive")
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
注意第二个写法——省略 switch 后的表达式等价于 switch true,每个 case 是 bool 表达式。这是 Go 替代 if-else-if 长链的"官方"写法。
# 6.4.4 类型 switch(预告,第 10 章详讲)
func describe(i any) {
switch v := i.(type) {
case nil:
fmt.Println("nil")
case int:
fmt.Printf("int: %d\n", v)
case string:
fmt.Printf("string: %q\n", v)
case []int:
fmt.Printf("[]int len=%d\n", len(v))
default:
fmt.Printf("unknown type %T\n", v)
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
i.(type) 只能用在 switch 的初始化子句里。第 10 章接口与多态会专门讲。
# 6.4.5 无条件 switch 替代 if-else 链
风格对照:
// ❌ 啰嗦
if status == "pending" {
handlePending()
} else if status == "running" {
handleRunning()
} else if status == "done" {
handleDone()
} else {
handleUnknown()
}
// ✅ Go 风格
switch status {
case "pending":
handlePending()
case "running":
handleRunning()
case "done":
handleDone()
default:
handleUnknown()
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
可读性、可扩展性、性能(编译器可生成跳表)都更好。3 个以上的 if-else-if 一律改 switch。
# 6.5 break / continue 与标签
裸 break / continue 只影响最内层循环。要跳出多层,加标签:
outer:
for i := 0; i < 5; i++ {
for j := 0; j < 5; j++ {
if i*j > 6 {
break outer // 跳出最外层
}
if j == i {
continue outer // 跳到外层下一轮
}
fmt.Println(i, j)
}
}
2
3
4
5
6
7
8
9
10
11
12
标签是"goto-but-controlled"——能干 goto 的事,但只能用在 break/continue/goto 上,且只能跳到自己所在或包围的循环 / switch / select 头部。
💡 何时用标签 break?
- 二维矩阵搜索:找到目标元素立即停搜
select内嵌for:要从case直接跳出整个 for比起在内层
if found { goto done },标签 break 可读性更好。
# 6.6 goto:什么时候才该用
goto 在 Go 里保留了,但有严格限制:
- 不能跳过变量声明
- 不能跳进/跳出函数
- 不能跨作用域(比如跳进一个
if块)
实战中用得最多的场景就两个:
场景 1:错误清理(替代 C 的"goto cleanup")
func process() error {
a, err := openA()
if err != nil { return err }
b, err := openB()
if err != nil { goto closeA }
c, err := openC()
if err != nil { goto closeB }
// ... 业务 ...
c.Close()
closeB:
b.Close()
closeA:
a.Close()
return err
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
但 Go 有更优雅的写法——defer。所以这个场景实际不需要 goto:
func process() error {
a, err := openA()
if err != nil { return err }
defer a.Close()
b, err := openB()
if err != nil { return err }
defer b.Close()
c, err := openC()
if err != nil { return err }
defer c.Close()
// ... 业务 ...
return nil
}
2
3
4
5
6
7
8
9
10
11
12
13
场景 2:状态机的状态跳转
写有限状态机时,每个状态用一个标签:
state1:
if cond1 { goto state2 }
goto state3
state2:
...
goto state1
state3:
...
2
3
4
5
6
7
8
但绝大多数状态机用 for { switch state { ... } } 也能写,可读性更高。
结论:Go 工程几乎不写 goto。除非 defer 不能解决的特殊场景(如嵌套循环里的"跳到外层 + 继续清理"),或为了和 C 代码风格保持一致。
# 6.7 没有 while / do-while / 三元
| 别的语言 | Go 等价 |
|---|---|
while (cond) {} | for cond {} |
do { ... } while (cond); | for { ...; if !cond { break } } |
cond ? a : b(三元) | if cond { return a }; return b |
为什么去掉?同一个理由——少即是多:
for一个关键字覆盖所有循环形态,新人学起来负担更小- 没有三元强制写完整 if,避免链式三元
a ? b : c ? d : e的可读性灾难
如果你真的觉得"三元真有必要",标准库里的写法是:
// max(a, b):Go 1.21+ 内置
m := max(a, b)
// 通用三元:包装成函数
func If[T any](cond bool, a, b T) T {
if cond { return a }
return b
}
x := If(score > 60, "pass", "fail")
2
3
4
5
6
7
8
9
注意 If 不是短路求值——参数 a, b 都会先求值。性能敏感场景仍然要写完整 if-else。
# 6.8 综合示例:FizzBuzz 与状态机
# 示例 1:FizzBuzz(基础流程语句训练)
// fizzbuzz/main.go
package main
import "fmt"
func main() {
for i := range 21 { // Go 1.22+
switch {
case i == 0:
continue
case i%15 == 0:
fmt.Println("FizzBuzz")
case i%3 == 0:
fmt.Println("Fizz")
case i%5 == 0:
fmt.Println("Buzz")
default:
fmt.Println(i)
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
涉及的本章知识点:
for i := range N(Go 1.22+ 整数遍历)- 无条件 switch 替代 if-else 链
continue跳过 0- 多值 case 略——这里没用到,但可改写为
case i%3 == 0, i%5 == 0:试试
# 示例 2:红绿灯状态机(标签 + switch)
// trafficlight/main.go
package main
import (
"fmt"
"time"
)
type State int
const (
Red State = iota
Green
Yellow
)
func (s State) String() string {
return [...]string{"Red", "Green", "Yellow"}[s]
}
func (s State) Duration() time.Duration {
return [...]time.Duration{5, 3, 1}[s] * time.Second
}
func (s State) Next() State {
return [...]State{Green, Yellow, Red}[s]
}
func main() {
state := Red
quit := time.After(20 * time.Second)
loop:
for {
select {
case <-quit:
fmt.Println("traffic light: shutting down")
break loop // 跳出 for 而不是只跳出 select
case <-time.After(state.Duration()):
fmt.Printf("%s -> %s\n", state, state.Next())
state = state.Next()
}
}
fmt.Println("done")
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
涉及的本章知识点:
| 知识点 | 体现位置 |
|---|---|
标签 loop: | 让 break loop 跳出 for,而不只是 select |
select 多路等待 | quit / 计时器 |
iota 枚举 | Red/Green/Yellow |
自定义类型 + String() | State.String() |
| 数组转表 | [...]State{...}[s] 替代多分支 if |
# 6.9 Go 新手陷阱 Top 5
# ❌ 陷阱 1:switch 期望穿透到下一 case
switch x {
case 1:
a()
case 2: // ❌ x=1 时永远不会执行 b()
b()
}
2
3
4
5
6
修复:要么用 fallthrough,要么改成多值 case 1, 2:。
# ❌ 陷阱 2:break 跳不出嵌套循环
for _, row := range matrix {
for _, v := range row {
if v == target {
break // ❌ 只跳出内层,外层继续
}
}
}
2
3
4
5
6
7
修复:
search:
for _, row := range matrix {
for _, v := range row {
if v == target {
break search
}
}
}
2
3
4
5
6
7
8
或用一个 found 标记 + return(更干净)。
# ❌ 陷阱 3:Go 1.21 及之前的循环变量陷阱
// Go 1.21 及之前
var fns []func()
for i := 0; i < 3; i++ {
fns = append(fns, func() { fmt.Println(i) })
}
for _, f := range fns { f() }
// 输出:3 3 3
2
3
4
5
6
7
Go 1.22+ 自动修复。如果你的项目还卡在 go 1.21,升级 go.mod 中的 go 版本到 1.22+ 就能解决。
# ❌ 陷阱 4:range 修改的是副本
type User struct{ Name string }
users := []User{{"A"}, {"B"}}
for _, u := range users {
u.Name = "X" // ❌ 改副本
}
fmt.Println(users) // [{A} {B}],没变
2
3
4
5
6
修复:
for i := range users {
users[i].Name = "X" // ✅ 通过索引改原元素
}
2
3
# ❌ 陷阱 5:for-range map 期望顺序稳定
for k := range m {
fmt.Println(k) // 顺序每次都不一样
}
2
3
修复:先排序 key,再按顺序遍历——见 5.4.4。
# 6.10 思考题
- 为什么 Go 把
for设计成"一个关键字打天下"?这种统一相对 C 的for/while/do-while三件套,在学习成本与表达力上的取舍? - Go 1.22 循环变量改动违反"Go 1 兼容承诺",但仍被推行——请查阅官方文档,列举支持这个决定的两个核心论据。
- 写一段代码:用带标签的
continue实现"跳过 i==j 时整个外层循环"。 switch x.(type)与switch v := x.(type)的区别?什么时候必须用后者?- 用 Go 实现一个
do-while:先执行一次再判断条件。提示:for的某种形态。 - 为什么
goto不能跳过变量声明?请用一个能编译失败的例子说明。 - Go 没有三元运算符。如果团队代码 base 大量
if cond { x = a } else { x = b },是否值得引入泛型If[T]函数?请从可读性、性能、调试三个角度分析。
# 6.11 推荐阅读
# 卷内
- 第 5 章 复合类型(map 遍历的随机性)
- 第 7 章 函数(
defer替代 goto cleanup) - 第 12 章 并发 goroutine(
select详讲) - 第 13 章 通道 channel(
for range ch)
# 跨卷
- 卷三第 12 章 迭代器与 range func(Go 1.23 新特性)
- 卷四第 6 章 常见反模式(goto 与"提前 return"哪个更好)