Skip to content
Go
Go 控制流

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 没有。