05 语句与控制流

Go 语句速查:if 带初始化语句、for 三种形态、range 在各类型上的语义、Go 1.22 循环变量变更、switch 与 fallthrough、标签跳转

05 语句与控制流

本页回答:Go 的控制流只有哪几种、range 在不同类型上分别给你什么、switch 为什么不需要 break、标签怎么用。

基线:Go 1.27.1。所有输出为本机实跑结果。 ⚠️ 本页涉及两个会改变旧代码行为的版本节点:Go 1.22 循环变量语义、Go 1.23 range over func。


语句家族总览

Go 的语句种类比 C 少得多:没有 while、没有 do-while、没有三元运算符、没有异常。

flowchart TB
    S(["语句 Statement"])
    S --> SIMPLE["简单语句"]
    S --> COMPOUND["复合语句"]

    SIMPLE --> S1["声明 var/const/type"]
    SIMPLE --> S2["赋值 = += -= ..."]
    SIMPLE --> S3["自增自减 ++ --<br/>只有后缀,且是语句不是表达式 ⚠️"]
    SIMPLE --> S4["表达式语句 f()"]
    SIMPLE --> S5["发送语句 ch <- v"]
    SIMPLE --> S6["空语句 ;"]

    COMPOUND --> C1["if / else"]
    COMPOUND --> C2["for / range"]
    COMPOUND --> C3["switch / type switch"]
    COMPOUND --> C4["select"]
    COMPOUND --> C5["go / defer"]
    COMPOUND --> C6["块 {}"]

    C1 & C2 & C3 --> J["跳转语句<br/>break continue goto fallthrough return"]

⚠️ i++ 是语句不是表达式,所以 x := i++ 和 f(i++) 都是语法错误 🛑。这是 Go 为了消除求值顺序歧义刻意做的取舍。


if:带初始化语句是 Go 的招牌

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
// 基本形态
if x > 0 {
	fmt.Println("positive")
} else if x == 0 {
	fmt.Println("zero")
} else {
	fmt.Println("negative")
}

// 🔥 招牌形态:初始化语句 + 条件
if v, err := compute(); err == nil {
	fmt.Println("ok:", v)
} else {
	fmt.Println("failed:", err)
}

初始化语句里声明的变量作用域覆盖整个 if-else 链,但不外泄——这是 Go 里控制变量生命周期最常用的手段:

flowchart LR
    A["if v, err := f(); err == nil {"] --> B["v, err 在此声明<br/>作用域 = if + else 全部"]
    B --> C["then 分支<br/>可用 v, err"]
    B --> D["else 分支<br/>可用 v, err ✅"]
    C & D --> E["if 结束后<br/>v 与 err 都不在作用域 ❌"]
写法好处
if err := f(); err != nilerr 不会污染函数作用域 🔥
if v, ok := m[k]; ok经典「判存在 + 取值」
if s := strings.TrimSpace(s); s != ""就地变换后判断,避免临时变量

⚠️ 别忘了 else 必须与 } 同行(分号插入规则,见 02)。


for:Go 只有一种循环

三种形态,全靠 for 一个关键字表达:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
// ① 三段式(类 C)
for i := 0; i < 3; i++ { }

// ② 条件式(类 while)
n := 0
for n < 3 { n++ }

// ③ 无限循环
for {
	if done() { break }
}
1
2
3
classic: 3
while-style: 3
infinite+break: 3
需求写法
经典计数循环for i := 0; i < n; i++
当 while 用for cond { }
死循环for { }
只计数不要值for range n 🆕 1.22 🔥
遍历容器for i, v := range xs

🆕 1.22:range 可以直接遍历整数

1
2
3
for i := range 3 {
	fmt.Print(i, " ")
}
1
0 1 2

⚠️ for i := range n 迭代的是 0..n-1,和「重复 n 次」语义一致。不要再写 for i := 0; i < n; i++ 来重复 n 次——但如果你需要 n 本身(比如倒序),还是得用三段式。


range:一张表看清所有类型

range 的返回值随容器类型变化,这是最容易记混的点:

容器类型第一个值第二个值说明
数组 / 切片 []Tint 下标T 元素副本副本!改它不影响原切片 ⚠️
字符串 stringint 字节下标rune 码点下标不连续 🔥
映射 map[K]VK 键V 值副本顺序随机 ⚠️
通道 chan TT 收到的值—直到 close 才结束 ⚠️
整数 intint 0..n-1—🆕 1.22
函数 func(func(K,V) bool)KV🆕 1.23 自定义迭代器
flowchart TB
    R(["for ... range X"])
    R --> T1["切片/数组 [N]T"] --> T1a["i, v := 下标, 元素副本"]
    R --> T2["字符串 string"] --> T2a["i, r := 字节下标, 码点 ⚠️"]
    R --> T3["映射 map"] --> T3a["k, v := 键, 值 · 顺序随机 ⚠️"]
    R --> T4["通道 chan"] --> T4a["v := 接收值 · close 才结束 ⚠️"]
    R --> T5["整数"] --> T5a["i := 0..n-1 · Go 1.22 🆕"]
    R --> T6["迭代器函数"] --> T6a["yield 驱动 · Go 1.23 🆕"]

字符串:下标是字节,值是码点

1
2
3
4
s := "héllo"
for i, r := range s {
	fmt.Print(i, ":", string(r), " ")
}
1
0:h 1:é 3:l 4:l 5:o

注意下标从 0,1,3,4,5 —— 跳过了 2,因为 é 占 2 字节。这就是「下标是字节、值是码点」的直接证据。知道了这个,你就能理解为什么「按索引访问第 n 个字符」在 Go 里必须绕道 []rune。

映射:顺序随机是刻意的

1
2
3
4
m := map[string]int{"a": 1, "b": 2}
for k := range m {
	fmt.Print(k, " ")
}
1
a b        ← 也可能是 "b a",每次运行都可能不同

⚠️ Go 故意随机化 map 迭代顺序,防止你依赖它。需要稳定顺序必须显式排序(slices.Sort 或 maps.Keys + 排序,见 14)。

通道:天然配合 close

1
2
3
4
5
6
7
ch := make(chan int, 3)
ch <- 1
ch <- 2
close(ch)
for v := range ch {
	fmt.Print(v, " ")
}
1
1 2

range 通道会在通道 close 且缓冲区取空后自动退出——这是「生产者-消费者」最简洁的写法。⚠️ 如果生产者忘记 close,消费者会永久阻塞(见 12 通道)。

元素是副本

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
xs := []struct{ N int }{{1}, {2}}
for _, x := range xs {
	x.N = 99        // ⚠️ 改的是副本,原切片纹丝不动
}
fmt.Println(xs)     // [{1} {2}]

for i := range xs {
	xs[i].N = 99    // ✅ 想改元素必须用下标
}
fmt.Println(xs)     // [{99} {99}]

🆕 1.22:循环变量每次迭代都是新的

这是会改变旧代码行为的语义变更,必须知道:

1
2
3
4
5
6
7
var fns []func()
for i := 0; i < 3; i++ {
	fns = append(fns, func() { fmt.Print(i, " ") })
}
for _, f := range fns {
	f()
}
1
0 1 2
版本行为
Go ≤ 1.21所有闭包共享同一个 i,输出 3 3 3 ⚠️
Go ≥ 1.22每次迭代新建变量,输出 0 1 2 ✅
flowchart LR
    subgraph OLD["Go ≤ 1.21:一个变量贯穿整个循环"]
        O1["i"] --> O2["迭代 1"] --> O3["迭代 2"] --> O4["迭代 3<br/>i 最终 = 3"]
        O4 --> O5["闭包全部捕获<br/>同一个 i → 3 3 3 ⚠️"]
    end
    subgraph NEW["Go ≥ 1.22:每次迭代新建"]
        N1["i₁"] --> N2["i₂"] --> N3["i₃"]
        N1 --> N4["闭包捕获 i₁ = 0 ✅"]
        N2 --> N5["闭包捕获 i₂ = 1 ✅"]
        N3 --> N6["闭包捕获 i₃ = 2 ✅"]
    end

⚠️ 这个变更由 go.mod 里的 go 指令控制:go 1.21 及更低仍用旧语义。升级 go 指令时如果程序依赖了旧的共享行为(极少见但存在),会静默改变结果。

for ... range 的 v 同理:

1
2
3
4
5
var fns2 []func()
for _, v := range []string{"a", "b", "c"} {
	fns2 = append(fns2, func() { fmt.Print(v, " ") })
}
for _, f := range fns2 { f() }
1
a b c

💡 于是老代码里那个 v := v 的补丁写法不再需要了 🪦:

1
2
3
4
for _, v := range xs {
	v := v   // 🪦 Go 1.22 起多余
	go func() { use(v) }()
}

🆕 1.23:range over func(自定义迭代器)

只要函数签名是 func(yield func(T) bool),就能被 range:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
import (
	"iter"
	"slices"
)

func count(n int) iter.Seq[int] {
	return func(yield func(int) bool) {
		for i := range n {
			if !yield(i) {
				return          // yield 返回 false 表示调用方 break 了,必须立即返回
			}
		}
	}
}

for v := range count(3) {
	fmt.Print(v, " ")
}
1
0 1 2

标准库已经全部支持:slices.Values、slices.Backward、maps.Keys、maps.Values、strings.SplitSeq 等都返回 iter.Seq。

1
2
3
for v := range slices.Backward([]int{1, 2, 3}) {
	fmt.Print(v, " ")
}
1
3 2 1

⚠️ 写生成器时的硬性约定:yield 返回 false 时必须立刻 return,否则调用方已经退出循环,你还在生产数据——轻则浪费,重则死锁。


switch:默认就不穿透

Go 的 switch 与 C 最大的区别:每个 case 结束自动 break,要穿透必须显式 fallthrough。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
switch 2 {
case 1:
	fmt.Print("one ")
	fallthrough
case 2:
	fmt.Print("two ")
	fallthrough
case 3:
	fmt.Print("three ")
case 4:
	fmt.Print("four ")
}
1
two three

几个必须记住的点:

点说明
自动 break不需要写 break,写了也对(但冗余)
fallthrough无条件进入下一个 case 的 body,不判断它的条件 ⚠️
fallthrough 不能在最后一个 case🛑 编译错误
case 可以是表达式列表case 1, 3, 5:
可以有初始化语句switch v := f(); v { } 🔥
无 tag switchswitch { case x > 0: } 等价于 if/else if 链
case 求值顺序从上到下,第一个命中的执行
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
// 无 tag:比一长串 else if 更整齐
switch {
case x < 0:
	fmt.Print("neg ")
case x == 0:
	fmt.Print("zero ")
default:
	fmt.Print("pos ")
}

// 带初始化 + 多值 case
switch v := 4; v {
case 1, 3, 5:
	fmt.Println("odd")
case 2, 4, 6:
	fmt.Println("even")
}
1
2
neg zero pos        ← 上面是对 x 依次取 -1、0、5 三次执行的结果
even                ← 4 命中 case 2, 4, 6

⚠️ 一个 switch 一次只执行一个分支,所以上面是把 x 取三个值各跑一次拼起来的输出;照抄示例时记得给 x 一个具体值。

类型 switch

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
switch v := x.(type) {
case nil:
	fmt.Print("nil ")
case int:
	fmt.Print("int:", v, " ")
case string:
	fmt.Print("string:", v, " ")
case []int:
	fmt.Print("slice len=", len(v), " ")
default:
	fmt.Printf("other %T ", v)
}
1
int:1 string:x slice len=2 other float64 nil

⚠️ 同理,类型 switch 一次也只命中一个 case。上面这一行是把 x 依次取 1、"x"、[]int{1,2}、3.5、nil 各跑一次的结果拼起来的(最后一次命中 case nil,输出 nil)。

细节说明
v 的类型在每个 case 里自动变成该 case 的类型 🔥
case nil匹配 nil 接口,不匹配「有类型的 nil」⚠️
多类型 casecase int, int64: 此时 v 保持原接口类型
default 的 v仍是接口类型

⚠️ 类型 switch 的 case nil 只在接口本身为 nil 时命中。装了一个 (*T)(nil) 的接口不会命中 case nil——这是 08 接口 里那个经典陷阱的另一面。


标签:跳出多层循环的唯一正道

Go 没有「带层数的 break」(没有 break 2),只能用标签。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
outer:
	for i := 0; i < 3; i++ {
		for j := 0; j < 3; j++ {
			if j == 2 {
				continue outer   // 跳到外层循环的下一次迭代
			}
			if i == 2 {
				break outer      // 彻底跳出外层循环
			}
			fmt.Print(i, j, " ")
		}
	}
1
0 0 0 1 1 0 1 1
语句效果
break跳出最内层 for/switch/select
break label跳出标签标注的那层 🔥
continue继续最内层 for 的下一次迭代
continue label继续标签标注的那层
goto label无条件跳转,不能跳过变量声明 ⚠️
fallthrough只用于 switch,进入下一个 case

⚠️ 标签必须紧贴在 for 语句上一行,且标签与 for 之间不能有其他语句。

goto:能用但不该常用

1
2
3
4
5
6
7
i := 0
loop:
	if i < 3 {
		fmt.Print("g", i, " ")
		i++
		goto loop
	}
1
g0 g1 g2

合法的 goto 限制(编译器强制):

限制说明
不能跳过变量声明goto 目标之后新声明的变量,跳过去会报错 🛑
不能跳进另一个函数标签是函数级的
不能跳进块内部只能跳到同层或外层
不能跳进 for 循环体常见错误

💭 实际代码里 goto 的合理用途基本只有两个:C 风格的多层清理(现已由 defer 取代),以及状态机重试。其他场合用循环和函数表达更清楚。


语句与作用域对照图

flowchart TB
    subgraph FUNC["函数体作用域"]
        V1["x := 1"]
        subgraph IF["if 语句块"]
            V2["if v, err := f(); err == nil {<br/>v/err 作用域覆盖 if+else"]
            subgraph IFBODY["then/else 分支"]
                V3["分支内可用 v, err"]
            end
        end
        subgraph FOR["for 语句块"]
            V4["for i := range n {<br/>i 每次迭代新建 🆕1.22"]
            subgraph FORBODY["循环体"]
                V5["闭包捕获的是<br/>本次迭代的 i ✅"]
            end
        end
        subgraph SW["switch 语句块"]
            V6["switch v := x.(type) {<br/>v 在每个 case 里是具体类型"]
        end
    end
    V1 -. "if 结束后 v/err 消失" .-> GONE["不可再访问 ❌"]

本页陷阱速查

症状实际原因正确做法
syntax error: unexpected ++i++ 是语句不是表达式拆成两行:x := i; i++
闭包里的循环变量全是最后一个值go.mod 的 go 指令 ≤ 1.21 用旧语义升级到 go 1.22+,或显式 v := v 🪦
range 里改元素没生效v 是元素副本用下标 xs[i].N = ...
按索引取字符串中文字符乱码range 的下标是字节[]rune(s) 或按字节下标切片
map 遍历顺序每次不同这是刻意随机化的需要稳定顺序就显式排序
range 通道卡死生产者没 close生产完毕后 close(ch)
fallthrough 落到了不该进的 case它不判断下一个 case 的条件只在确实要顺序穿透时使用
case nil 不匹配 (*T)(nil)nil 接口 ≠ 有类型的 nil 接口在 case 里再判 v == nil
break 只跳出了一层嵌套循环需要标签break outer
goto 报 jumps over declaration跳过了变量声明把声明移到标签之前
for range n 行为像旧代码go 指令低于 1.22,range int 不支持升级 go 指令
自定义迭代器 yield 返回 false 后仍继续没有立刻 returnif !yield(v) { return }

📘 官方参考:Spec — Statements、Go 1.22 Release Notes — for loops、Go 1.23 Release Notes — range over func、Go Blog — Range Over Function Types

➡️ 上一节:04 变量·常量·iota | 下一节:06 函数·方法·defer

最后修改 September 21, 2026: 更新 (ac821931b)