Go 控制流
if/else、for 循环(含整数 range 与 range-over-func 迭代器)、switch/case、select、goto 与 fallthrough 等 Go 控制结构速查(基线 Go 1.27)。
1. if / else
// 基本形式
if x > 0 {
fmt.Println("positive")
}
// if-else if-else
if x < 0 {
fmt.Println("negative")
} else if x == 0 {
fmt.Println("zero")
} else {
fmt.Println("positive")
}
// if 带初始化语句(Go 特有)
if err := doSomething(); err != nil {
return err
}
// err 作用域仅限于 if-else 块内
if v, ok := m[key]; ok { // 惯用法:if + map 取值判断
fmt.Println(v)
}最佳实践:充分利用
if带初始化语句的特性——将变量作用域最小化,减少外部可见性。这是 Go 的惯用模式。if err != nil是 Go 中最常见的代码片段。
注意:
else必须紧跟在}之后同行,否则 Go 编译器会插入隐式分号导致编译失败。这与 Go 的"左括号不能换行"规则一致。
if 作用域变量
if f, err := os.Open("file.txt"); err != nil {
return err
} else {
// f 在 else 中也可见
fmt.Println(f.Name())
}
// f 和 err 在 if-else 块外不可见2. for 循环
Go 中只有 for 一种循环关键字,但能表达所有循环形式:
// 经典 for 循环(初始化; 条件; 后置)
for i := 0; i < 10; i++ {
fmt.Println(i)
}
// while 风格(仅有条件)
for condition {
// ...
}
// 无限循环
for {
// ...
if done {
break
}
}
// range 遍历
for i, v := range slice { } // 切片:索引 + 值
for k, v := range m { } // map:键 + 值
for i, r := range "你好" { } // 字符串:字节索引 + rune
for i, v := range ch { } // channel:值(直到关闭)for range 进阶
// 忽略索引/键
for _, v := range items {
fmt.Println(v)
}
// 仅取索引/键
for i := range items {
items[i] = items[i] * 2
}
// 两个都忽略,只数次数
for range items {
count++
}整数 range(Go 1.22)
Go 1.22 起,range 可以直接跟一个整数,语义上等价于 for i := 0; i < n; i++——类似 Python 的 range(n):
for i := range 10 { // 0, 1, 2, ..., 9
fmt.Println(i)
}
for range 3 { // 不带变量,纯执行 3 次(实测)
count++
}注意:
n可以是任意整数类型或整数常量,但不是半开区间的概念——只接受上界。n <= 0时循环体一次都不执行(for i := range 0与for i := range -5均为 0 次迭代,实测),不会报错也不会死循环。语言门控上,go 指令低于 1.22 的模块会报cannot range over 10 (untyped int constant): requires go1.22 or later(实测)。
循环变量:每轮独立(Go 1.22)
for _, v := range items {
go func() {
fmt.Println(v) // Go 1.22+ 每次迭代 v 是新变量
}()
}Go 1.22 起,for 的循环变量在每次迭代都是新变量,直接在闭包/goroutine 里捕获是安全的,不再需要 v := v shadow 一行。该语义同样由模块 go 指令门控(go ≥ 1.22 才生效),存量低版本模块行为不变,详见 10-版本变迁.md。
break / continue + 标签
outer:
for i := 0; i < 3; i++ {
for j := 0; j < 3; j++ {
if i == 1 && j == 1 {
break outer // 跳出外层循环
}
}
}
inner:
for i := 0; i < 10; i++ {
if i%2 == 0 {
continue inner // 跳到外层的下一次迭代
}
}3. range-over-func:迭代器循环(Go 1.23)
Go 1.23 把 range 的右侧扩展到函数值:只要一个函数的类型是 iter.Seq[T] 或 iter.Seq2[K, V],就能直接放进 for range。这让"自定义遍历逻辑"第一次有了标准姿势——以前要么返回大切片让调用方吃内存,要么传 channel 加 goroutine,现在一个闭包就够。
类型定义就两个(import "iter"):
type Seq[T any] func(yield func(T) bool) // 单值:iter.Seq[int] 等
type Seq2[K, V any] func(yield func(K, V) bool) // 键值对:maps 包的迭代器就是它规则的精髓在于 yield 的返回值:迭代器每产出一个值,就调用 yield(v);yield 返回 false 意味着消费方不想再要了(break、return 或提前退出),此时迭代器必须尽快 return,不要再调用 yield——这就是提前退出的协作协议。
可运行示例
package main
import (
"fmt"
"iter"
)
// upTo 依次产出 0..n-1,类型是 iter.Seq[int]
func upTo(n int) iter.Seq[int] {
return func(yield func(int) bool) {
for i := 0; i < n; i++ {
if !yield(i) { // 消费方 break 后 yield 返回 false
return // 必须尊重它,立即退出
}
}
}
}
// squares 产出 (key, value) 对,类型是 iter.Seq2[string, int]
func squares(n int) iter.Seq2[string, int] {
return func(yield func(string, int) bool) {
for i := 1; i <= n; i++ {
if !yield(fmt.Sprintf("k%d", i), i*i) {
return
}
}
}
}
func main() {
sum := 0
for v := range upTo(5) {
sum += v
}
fmt.Println("seq sum:", sum) // seq sum: 10
n := 0
for range upTo(100) {
n++
if n == 3 {
break // break 触发迭代器提前退出
}
}
fmt.Println("early break yielded:", n) // early break yielded: 3
for k, v := range squares(3) {
fmt.Printf("seq2: %s=%d\n", k, v)
}
}实测输出(go1.27.0):
seq sum: 10
early break yielded: 3
seq2: k1=1
seq2: k2=4
seq2: k3=9要点与边界
- break 即协作退出:
break让yield返回false;迭代器里忘记检查返回值、yield之后继续产出是 bug 温床。continue同理——它会继续拉下一个值。 - defer 正常工作:迭代器函数体内注册的
defer(如释放游标、关文件)在yield返回false后的退出路径上会执行,前提是迭代器真的 return 了。 - 不想写
if !yield(...)样板,用iter.Pull/iter.Pull2把"主动拉取"风格函数包装成Seq,错误码/停止条件由包装层处理;日常实现简单迭代器用上面的 yield 协议即可。 - 标准库已配齐一批迭代器(可直接 range,不必自己写):
strings.SplitSeq/FieldsSeq(Go 1.24 起,语言门控实测requires go1.24 or later)、maps.Keys/maps.Values/maps.All、slices.Chunk等,见09-标准库速览.md。 - 语言门控:go 指令低于 1.23 时报
cannot range over seq() (value of func type iter.Seq[int]): requires go1.23 or later(实测)。 for i, v := range seqFn需要seqFn的每个值都能落到循环变量;Seq2的两个返回值顺序固定为(K, V)。
与旧写法的对比
| 写法 | 提前退出 | 惰性 | 需要 goroutine/channel |
|---|---|---|---|
返回 []T |
全量生成后浪费 | 否 | 否 |
| channel 生产者 | 需 close + 消费方 drain |
是 | 是 |
iter.Seq(1.23) |
yield 返回 false 即停 |
是 | 否 |
实战建议
优先把"逐个产出元素"的函数定义成返回 iter.Seq[T],调用方 for range 直接消费——惰性、可提前退出、无额外 goroutine。给存量类型加迭代能力时,参照标准库命名:All / Values / SplitSeq 这类后缀已成惯例。
4. switch
Go 的 switch 比 C/Java 更强大,不需要 break,支持多种匹配形式:
基本 switch
// 值匹配(不需要 break,默认不会 fall through)
switch day {
case "Monday":
fmt.Println("周一")
case "Saturday", "Sunday": // 多个值逗号分隔
fmt.Println("周末")
default:
fmt.Println("工作日")
}
// 无表达式 switch(替代 if-else 链)
switch {
case score >= 90:
grade = "A"
case score >= 80:
grade = "B"
case score >= 70:
grade = "C"
default:
grade = "F"
}switch 带初始化语句
switch lang := os.Getenv("LANG"); lang {
case "zh_CN.UTF-8":
fmt.Println("中文环境")
case "en_US.UTF-8":
fmt.Println("English")
default:
fmt.Printf("Unknown: %s\n", lang)
}fallthrough(显式穿透到下个 case)
switch n {
case 1:
fmt.Println("one")
fallthrough // 强制执行下一个 case(不判断条件)
case 2:
fmt.Println("one or two") // n=1 也会执行到此处
case 3:
fmt.Println("three")
}注意:
fallthrough只穿透一个 case,且不能用在最后一个 case 中。在日常开发中极少使用,大多数时候用逗号多值匹配即可。
5. type switch(类型分支)
func describe(i interface{}) {
switch v := i.(type) {
case int:
fmt.Printf("int: %d\n", v)
case string:
fmt.Printf("string: %s\n", v)
case bool:
fmt.Printf("bool: %t\n", v)
case nil:
fmt.Println("nil")
default:
fmt.Printf("unknown type: %T\n", v)
}
}最佳实践:
switch v := i.(type)是最安全、最常用的类型判断方式。比大量if v, ok := x.(T); ok {}更清晰易懂。更多接口动态判定见06-接口与泛型.md。
6. select(多路 channel 选择)
select 用于同时等待多个 channel 操作:
select {
case msg := <-ch1:
fmt.Println("from ch1:", msg)
case msg := <-ch2:
fmt.Println("from ch2:", msg)
case ch3 <- value:
fmt.Println("sent to ch3")
case <-time.After(1 * time.Second):
fmt.Println("timeout")
default:
fmt.Println("no channel ready")
}| 特性 | 说明 |
|---|---|
| 随机公平 | 多个 case 同时就绪时,随机选择一个执行 |
| default 非阻塞 | 有 default 时 select 不阻塞 |
| 无 default | select 阻塞直到某个 case 就绪 |
| 空 select | select {} 永久阻塞(常用于 main 中阻止退出) |
| channel 关闭 | 从已关闭的 channel 读取立即返回零值,可无限次触发 |
详见 08-并发编程.md。
7. goto
Go 支持 goto,但极少使用:
func cleanup() {
f, err := os.Open("file.txt")
if err != nil {
goto HandleError
}
// ... 正常处理
return
HandleError:
log.Println("error:", err)
// 错误处理逻辑
}注意:
goto不能跳过变量声明。绝大多数场景下,defer+if err != nil { return err }替代goto更清晰可控。仅在复杂的状态机或解析器中偶尔用到。
8. 常见陷阱
8.1 switch 不会 fall through(与 C 不同)
Go 的 case 匹配后自动退出,不像 C 需要 break。如果需要穿透到下个 case,必须显式写 fallthrough。
8.2 range 循环变量复用(go 指令 ≤ 1.21 的模块)
// Go 1.21 及之前语言版本:错误!
for _, v := range []int{1, 2, 3} {
go func() { fmt.Println(v) }() // 可能全部输出 3(共享变量)
}
// Go 1.22+ 语言版本:安全,每次迭代 v 是新变量语义由模块 go 指令决定,不看编译器版本——低版本模块升级工具链后行为不变,排查这类 bug 先看 go.mod。
8.3 迭代器不检查 yield 返回值
// 错误写法:消费方 break 后还继续 yield
func bad(n int) iter.Seq[int] {
return func(yield func(int) bool) {
for i := 0; i < n; i++ {
yield(i) // 返回值被忽略
}
}
}消费方 break 之后 yield 返回 false,迭代器却继续跑完全部数据——白白消耗 CPU、甚至产生副作用。必须 if !yield(i) { return }(见第 3 节)。
8.4 从已关闭 channel 读取
ch := make(chan int)
close(ch)
v, ok := <-ch // v=0, ok=false(无限次返回零值)
// 在 select 中会不断触发8.5 对 nil channel 操作
| 操作 | nil channel 行为 |
|---|---|
<-ch 读取 |
永久阻塞 |
ch <- v 发送 |
永久阻塞 |
close(ch) |
panic |
select 中的 nil case |
被忽略(不会触发) |
8.6 for range 遍历 map 顺序不确定
m := map[string]int{"a": 1, "b": 2, "c": 3}
for k, v := range m {
fmt.Println(k, v) // 每次运行输出顺序可能不同
}
// Go 甚至会故意随机化遍历起始位置,防止开发者依赖顺序
// 要稳定顺序:先收集 keys 再 slices.Sort,见 `04-复合类型.md`8.7 整数 range 没有起点/步长语法
for i := range n 固定从 0 到 n-1、步长 1。需要其他区间时老老实实写经典 for i := start; i < end; i += step,别误以为存在 range(a, b) 或 range 1..n 的写法——Go 没有。