Go for 循环:统一语法、range、迭代器与控制流
面向有 Java 经验的开发者,基于 Go 1.26。
Go 只有一个循环关键字:for。传统三段式循环、条件循环、无限循环,以及集合和迭代器的遍历,都由它完成。语法统一并不意味着语义也简单:数组和切片的 range 会复制元素,map 的顺序不确定,字符串按 UTF-8 解码,channel 要等到关闭才会结束循环。Go 1.22 又改变了循环变量的生命周期并加入整数 range,Go 1.23 则进一步允许对迭代函数执行 range。
因此,这一章不会只罗列 for 的几种写法,而是把它放回控制流和迭代协议中理解。
目录
1. 先建立心智模型
理解 for 可以分成两个层次:
- 基础循环负责“何时继续、何时退出”;
range负责从某种数据源取得下一组迭代值。
基础形式:
for init; condition; post {
body
}省略 init 和 post,就是条件循环:
for condition {
body
}连条件也省略,就是无限循环:
for {
body
}range 形式:
for index, value := range source {
body
}source 不只可以是容器。截至 Go 1.26,range 支持:
| range 对象 | 第一迭代值 | 第二迭代值 |
|---|---|---|
array / *array | 索引 | 元素副本 |
| slice | 索引 | 元素副本 |
| string | UTF-8 字节索引 | rune |
| map | 键 | 值 |
| channel | 接收到的元素 | 无 |
| integer | 从 0 开始的整数 | 无 |
func(func() bool) | 无 | 无 |
func(func(V) bool) | V | 无 |
func(func(K, V) bool) | K | V |
沿着这张表继续看,许多边界最终都可以归结为三个问题:
- 每次拿到的是原值还是副本?
- 迭代顺序是否有保证?
- 循环怎样得知数据结束,提前停止又怎样通知数据源?
2. 三种基本 for
2.1 三段式
for i := 0; i < 10; i++ {
fmt.Println(i)
}执行顺序是:
i := 0只执行一次;- 每轮开始检查
i < 10; - 条件为真时执行循环体;
- 执行
i++; - 回到步骤 2。
与 Java 不同,这三部分不加圆括号。初始化和后置部分必须是简单语句,不能写多个用逗号分隔的表达式;需要同时更新多个值时,可以使用平行赋值:
for left, right := 0, len(values)-1; left < right; left, right = left+1, right-1 {
values[left], values[right] = values[right], values[left]
}2.2 条件循环
Go 没有 while,直接省略前后两段:
scanner := bufio.NewScanner(r)
for scanner.Scan() {
process(scanner.Text())
}
if err := scanner.Err(); err != nil {
return err
}条件在每轮开始时都会重新求值,因此可以直接放置会推进状态的函数调用,例如 scanner.Scan() 或解析器的 Next()。
2.3 无限循环
for {
conn, err := listener.Accept()
if err != nil {
return err
}
go handle(conn)
}无限循环通常依靠 break、return、context 取消、channel 关闭或错误结束。生产服务不能只假设它会“永远运行”,对应的关闭路径也要一并设计。
3. 初始化、条件与后置语句
3.1 三部分都可以省略
for ; i < limit; {
i += step
}上面合法,但通常写成更自然的条件循环:
for i < limit {
i += step
}仅保留分号而省略条件,条件视为 true:
for i := 0; ; i++ {
if done(i) {
break
}
}3.2 ++ 和 -- 是语句
Go 的 i++、i-- 没有表达式值:
i++
// j := i++ // 编译错误它们只能作为语句,不能嵌入函数参数或复杂表达式,也没有前置 ++i。这减少了一个表达式中同时读写变量的歧义。
3.3 作用域
初始化语句声明的变量只在 for 语句内可见:
for i := 0; i < 3; i++ {
fmt.Println(i)
}
// fmt.Println(i) // 编译错误如果循环后还要用计数器,先在外层声明:
i := 0
for ; i < len(values) && valid(values[i]); i++ {
// ...
}
fmt.Println("停在:", i)4. break、continue、return 与 goto
4.1 break
不带标签的 break 终止最内层 for、switch 或 select:
for _, value := range values {
if value < 0 {
break
}
fmt.Println(value)
}4.2 continue
continue 跳过本轮剩余部分:
for _, path := range paths {
if strings.TrimSpace(path) == "" {
continue
}
if err := process(path); err != nil {
return err
}
}在三段式循环里,continue 之后仍会执行后置语句,再检查条件。
4.3 return
return 结束整个函数,并执行该函数已经注册的 defer:
for _, item := range items {
if err := validate(item); err != nil {
return fmt.Errorf("校验 %q: %w", item.ID, err)
}
}
return nil是否遇错即停由业务语义决定。批处理若要汇总错误,可以使用 errors.Join 或定义带索引的错误结果。
4.4 goto 的边界
Go 保留 goto,但它不能跳过变量声明进入该变量的作用域,也不能跳入另一个块。少数资源清理或生成代码可能用到,常规业务循环优先使用函数提取、break、continue 和 return。
for {
if fatal() {
goto Done
}
}
Done:
cleanup()如果标签只是为了统一清理,defer cleanup() 往往更可靠。
5. 标签与多层控制流
标签是标识符加冒号,可以作为 break 和 continue 的目标。
5.1 跳出多层循环
Found:
for row := 0; row < len(matrix); row++ {
for col := 0; col < len(matrix[row]); col++ {
if matrix[row][col] == target {
fmt.Println(row, col)
break Found
}
}
}不带标签的 break 只结束内层循环。
5.2 continue 外层循环
Rows:
for _, row := range rows {
for _, value := range row {
if value < 0 {
continue Rows
}
}
accept(row)
}标签应紧邻目标语句,名字表达意图,如 Rows、Search、Retry。标签过多通常说明一个函数承担了太多阶段,应考虑提取函数。
5.3 switch / select 嵌在循环中
这是标签最有实际价值的场景:
Loop:
for {
select {
case value, ok := <-ch:
if !ok {
break Loop
}
use(value)
case <-ctx.Done():
return ctx.Err()
}
}
return nilbreak 不带标签只会离开 select,下一轮 for 仍会继续。
6. range 的统一规则
range 表达式通常在进入循环前求值一次:
for i := range makeSlice() {
fmt.Println(i)
}makeSlice() 只调用一次,不会在每轮重新调用。规范还规定了一个优化例外:如果最多只有一个迭代变量,并且 len(rangeExpression) 是常量,range 表达式可能完全不求值。
迭代变量有三种写法:
for i, value := range values { // 每轮声明/赋值
_, _ = i, value
}
var i int
var value Item
for i, value = range values { // 写入预先存在的变量
_, _ = i, value
}
for range values { // 不需要任何迭代值
do()
}只要第一个值:
for i := range values {
fmt.Println(i)
}只要第二个值时用空白标识符:
for _, value := range values {
fmt.Println(value)
}末尾的空白标识符等价于省略:
for key, _ = range m {}
// 等价于
for key = range m {}7. 数组与切片
7.1 索引和值
values := []string{"a", "b", "c"}
for i, value := range values {
fmt.Println(i, value)
}value 是元素的副本。修改它不会修改 slice:
numbers := []int{1, 2, 3}
for _, number := range numbers {
number *= 10
}
fmt.Println(numbers) // [1 2 3]按索引修改:
for i := range numbers {
numbers[i] *= 10
}若元素是指针,复制的是指针值,通过指针修改目标仍然可见:
for _, user := range users { // users 是 []*User
user.Active = true
}7.2 大 struct 的复制成本
type Record struct {
ID uint64
Payload [4096]byte
}
for _, record := range records {
consume(record)
}每轮会取得元素值。编译器可能优化部分复制,但不能靠猜测保证。若只读大对象,可以按索引取地址:
for i := range records {
consumePtr(&records[i])
}是否更快要 benchmark;指针会增加别名和逃逸可能,API 语义应优先。
7.3 数组表达式可能整体复制
数组是值类型。range 一个数组值时,range 表达式会被求值并保存,可能复制整个数组:
var array [1024]Record
for i := range array {
_ = i
}只使用索引且数组长度为常量时,规范允许不求值 range 表达式;一旦取第二值,数组的值语义便不可忽略。遍历大数组时可以 range 指针:
for i, value := range &array {
_, _ = i, value
}7.4 循环中 append
slice 的 range 在开始时确定要遍历的长度。循环里 append 不会让当前循环自动扩展:
values := []int{1, 2, 3}
for _, value := range values {
values = append(values, value)
}
fmt.Println(len(values)) // 6,循环只执行原来的 3 次如果 append 没触发扩容,修改共享底层数组可能影响后续尚未读取的元素;触发扩容后又是另一种行为。不要写依赖容量偶然值的逻辑。需要队列式“边遍历边增长”时,使用显式索引条件:
for i := 0; i < len(queue); i++ {
queue = append(queue, expand(queue[i])...)
}同时必须有终止条件,避免无界增长。
8. 字符串与 Unicode
range string 按 UTF-8 解码,每轮得到:
- 当前 rune 的起始字节索引;
- 解码后的 rune。
text := "Go语言"
for index, r := range text {
fmt.Printf("byte=%d rune=%c U+%04X\n", index, r, r)
}索引不是“第几个字符”,而是字节偏移。len(text) 也是字节数:
text := "A中"
fmt.Println(len(text)) // 4
for i, r := range text {
fmt.Println(i, r) // 0 'A';1 '中'
}遇到非法 UTF-8 字节序列,range 每次产生 utf8.RuneError,并根据解码结果前进。若业务需要区分文本是否合法,先用 utf8.ValidString:
if !utf8.ValidString(text) {
return errors.New("文本不是合法 UTF-8")
}rune 也不等于用户看到的“字符”。组合音标、emoji 和旗帜可能由多个 code point 组成。标准库没有直接提供完整的 grapheme cluster 分割;界面长度需求要采用符合 Unicode 文本边界的专门方案。
只需要 rune 而不需要索引:
for _, r := range text {
fmt.Println(r)
}需要逐字节:
for i := 0; i < len(text); i++ {
fmt.Printf("%02x\n", text[i])
}9. map
9.1 顺序不确定
for key, value := range settings {
fmt.Println(key, value)
}map 的迭代顺序未指定,不保证每次相同,也不能依赖插入顺序。稳定输出需要收集并排序键:
keys := make([]string, 0, len(settings))
for key := range settings {
keys = append(keys, key)
}
slices.Sort(keys)
for _, key := range keys {
fmt.Println(key, settings[key])
}9.2 迭代期间删除和新增
规范对当前迭代尚未到达的元素有明确边界:
- 删除一个尚未到达的 entry,它不会再被产生;
- 新增 entry 可能被产生,也可能不被产生;
- 行为可以因 entry、因迭代而不同。
因此“遍历并删除不满足条件的键”是可定义的:
for key, value := range cache {
if expired(value) {
delete(cache, key)
}
}而“遍历时新增工作,期待本轮处理”没有确定语义,应分成两个阶段或使用显式队列。
9.3 并发不是安全的
一个 goroutine 写 map,同时另一个 goroutine 读取或遍历,会产生数据竞争,运行时还可能直接报 concurrent map iteration and map write。处理方式可以是 mutex、单一所有者 goroutine,或适合特定负载的 sync.Map。程序即使“没有崩”,race detector 报告的竞争也不能留着:
go test -race ./...10. channel
对 channel 做 range 只有一个迭代值:
for value := range ch {
process(value)
}循环不断接收,直到 channel 被关闭并且缓冲区已排空。它不会因为暂时没有值就结束。
两个重要边界:
- nil channel 上 range 永久阻塞;
- 没有人关闭的 channel 在生产者退出后会让消费者永久等待。
典型生产者:
func Produce(ctx context.Context) <-chan int {
out := make(chan int)
go func() {
defer close(out)
for i := range 10 {
select {
case out <- i:
case <-ctx.Done():
return
}
}
}()
return out
}由创建并发送 channel 的一方负责关闭,接收方通过 range 消费。若消费者可能提前返回,必须用 context 或另一条停止协议通知生产者,否则生产者可能永远阻塞在发送上。
11. 整数 range
Go 1.22 起可以 range 整数:
for i := range 5 {
fmt.Println(i) // 0 1 2 3 4
}若整数 n <= 0,循环执行零次:
for range -3 {
panic("不会执行")
}迭代值从 0 开始,逐次加 1,到 n-1 为止。类型规则要区分是否预先声明变量:
for i := range uint8(3) {
// i 的类型是 uint8
fmt.Printf("%T %d\n", i, i)
}
var j int64
for j = range 3 {
fmt.Println(j)
}整数 range 适合“重复 n 次”和零起点索引。它不是任意步长区间语法;从 10 到 20、倒序或步长 2,仍用三段式:
for i := 10; i <= 20; i += 2 {
fmt.Println(i)
}不要为了使用新语法把清晰的集合遍历改成下标 range。读取 slice 元素时,for i, value := range values 仍更直接。
12. 函数迭代器
Go 1.23 起,range 可以消费特定函数签名:
func(func() bool)
func(func(V) bool)
func(func(K, V) bool)标准库 iter 包给它们命名:
type Seq[V any] func(yield func(V) bool)
type Seq2[K, V any] func(yield func(K, V) bool)12.1 编写 Seq
func Backward[V any](values []V) iter.Seq[V] {
return func(yield func(V) bool) {
for i := len(values) - 1; i >= 0; i-- {
if !yield(values[i]) {
return
}
}
}
}
for value := range Backward([]string{"a", "b", "c"}) {
fmt.Println(value) // c b a
}range 循环每次通过编译器提供的 yield 接收值。若循环体执行 break,yield 返回 false,迭代器必须停止,不得再次调用它。
12.2 continue、break 与 return
for value := range sequence {
if value < 0 {
continue
}
if value > 100 {
break
}
use(value)
}对调用者而言,控制流和普通 range 一致。对迭代器作者而言:
continue表现为本次 yield 返回后继续请求下一项;break使 yield 返回 false;return同样要求迭代停止;- yield 返回 false 后再次调用是协议错误。
12.3 Seq2
func Enumerate[V any](values []V) iter.Seq2[int, V] {
return func(yield func(int, V) bool) {
for i, value := range values {
if !yield(i, value) {
return
}
}
}
}Seq2 只支持两个值。需要三个以上字段时,用一个 struct 作为迭代值。
12.4 push、pull 与一次性序列
Seq 是 push 形式:迭代器主动调用 yield。iter.Pull 可以转换成 pull 形式:
next, stop := iter.Pull(sequence)
defer stop()
for {
value, ok := next()
if !ok {
break
}
use(value)
}提前停止时必须调用 stop,以便迭代器释放 goroutine 或其他资源。迭代器是否可以重复遍历、是否只允许一次、是否会暴露底层可变值,属于 API 契约,应在文档中说明。
13. Go 1.22 循环变量语义
13.1 旧问题
旧语义下,使用 := 声明的循环变量在所有迭代间复用,闭包可能都观察到最后一个值:
for _, value := range values {
go func() {
fmt.Println(value)
}()
}Go 1.22 的新语义让每次迭代拥有新的循环变量。对于语言版本为 Go 1.22 及以上的模块,上面的每个闭包捕获对应迭代的 value。
13.2 不只影响 goroutine
保存地址或普通闭包也受影响:
var functions []func() string
for _, value := range []string{"a", "b", "c"} {
functions = append(functions, func() string { return value })
}新语义得到 a、b、c。它不仅是并发修复,而是变量身份发生变化。
13.3 = 形式仍复用已有变量
新语义针对循环声明的变量。若使用赋值形式写入循环外的变量,它仍是同一个变量:
var value string
var functions []func() string
for _, value = range []string{"a", "b", "c"} {
functions = append(functions, func() string { return value })
}这些闭包仍共享外层 value。最稳妥的判断方式不是背版本口诀,而是看变量在哪里声明。
13.4 模块语言版本
语义由包对应的语言版本控制,通常来自 go.mod 的 go 行,不只是安装了哪版工具链。维护旧模块时运行:
go version
go env GOMOD
go vet ./...升级 go 行前应执行测试,尤其关注过去依赖循环变量复用的罕见代码。value := value 这类旧式防捕获写法在新版本中通常冗余,但保留它一般不改变正确性。
14. 修改、删除与并发边界
14.1 range 值是副本
对 []struct:
for _, user := range users {
user.Active = true // 只改副本
}应写:
for i := range users {
users[i].Active = true
}对 map[K]Struct,map 元素不可寻址,通常取出、修改、写回:
for key, user := range users {
user.Active = true
users[key] = user
}或者 map 保存指针,但那会改变共享、nil、并发和所有权语义,不能只为了少一行赋值就改数据模型。
14.2 slice range 与并发
一个 goroutine range slice,另一个 goroutine append 或修改同一底层数组,通常是数据竞争。slice header 复制不代表底层数组隔离。需要锁、不可变快照或所有权转移:
mu.RLock()
snapshot := slices.Clone(values)
mu.RUnlock()
for _, value := range snapshot {
use(value)
}克隆有分配和复制成本,但把锁持有时间与慢处理解耦。是否合适取决于数据量和一致性要求。
14.3 闭包并发还要看元素本身
Go 1.22 解决了循环变量身份问题,没有自动解决被指向对象的数据竞争:
for _, user := range users {
go func() {
user.Active = true
}()
}如果 users 是 []*User,每轮指针变量独立,但多个元素可能指向同一个 User,或其他 goroutine 也在访问它。仍要建立同步和生命周期协议。
15. 性能与工程实践
15.1 先选语义,再做基准
下标循环与 range 的性能通常接近,编译器能消除许多边界检查。不要凭旧文章统一改成下标。值得关注的是:
- range 大 struct 的复制;
- 循环中分配、字符串拼接和
fmt; - 循环中重复计算不变量;
- 内层线性查找形成 O(n²);
- goroutine 无界创建;
- 锁持有期间执行 I/O;
- map 顺序稳定化所需的排序成本。
用 benchmark 和 profile 验证:
go test -bench=. -benchmem ./...
go test -run=^$ -bench=BenchmarkLoop -memprofile=mem.out
go tool pprof mem.out15.2 预分配
知道输出上限时预分配:
result := make([]Item, 0, len(input))
for _, item := range input {
if keep(item) {
result = append(result, transform(item))
}
}容量是上限估计,不是必须填满的长度。不要 make([]Item, len(input)) 后再 append,那会留下前半段零值。
15.3 循环内 defer
defer 在外围函数返回时执行,不在每次迭代结束时执行:
for _, path := range paths {
file, err := os.Open(path)
if err != nil {
return err
}
defer file.Close() // 所有文件都要等外层函数结束
}提取单次处理函数:
func processFile(path string) error {
file, err := os.Open(path)
if err != nil {
return err
}
defer file.Close()
return consume(file)
}这样每轮函数返回就关闭资源,错误路径也不会漏。
15.4 并发循环要有上限
for _, item := range items {
go process(item)
}输入很大时会创建同等数量 goroutine。使用固定 worker、信号量或任务组限制并发,并定义错误取消、结果收集和 panic 策略。循环变量已安全不等于并发模型已安全。
16. 与 Java 的对照
| Java | Go | 说明 |
|---|---|---|
for (init; cond; post) | for init; cond; post | Go 无圆括号 |
while (cond) | for cond | Go 无 while 关键字 |
for (;;) | for {} | 无限循环 |
| enhanced for | for _, v := range values | Go 常同时提供索引 |
break label | break Label | Go 标签写在目标语句前 |
continue label | continue Label | 用于继续外层循环 |
Iterator<T> | iter.Seq[T] / range func | Go 1.23 的 push 迭代协议 |
IntStream.range(0, n) | range n | Go 1.22,零到 n-1 |
String.codePoints() | range string | Go 返回字节索引和 rune |
Map 遍历 | range map | 顺序未指定,修改规则不同 |
Java enhanced for 的局部变量也是元素值;Go 对 slice struct 元素的 range 变量同样不是“元素引用”。两边都不应通过给循环变量重新赋值来期待修改容器,但 Java 对对象字段的修改与 Go 的指针/值选择需要分别分析。
17. 常见误区
- 以为 Go 有
while或do/while:条件循环用for condition;至少执行一次可先执行主体再判断,或用带 break 的 for。 - 忘记 range 的元素值是副本:修改 slice struct 元素要按索引。
- 把 string 索引当字符序号:range 索引是 UTF-8 字节偏移。
- 依赖 map 遍历顺序:需要稳定输出就排序键。
- 遍历 map 时新增元素,并期待本轮处理:新元素是否出现未指定。
- range channel 后程序不结束:发送方没有关闭,或消费者缺少取消路径。
- 关闭 channel 后继续 select 接收:关闭 case 永远就绪,应检查
ok并设为 nil。 - 认为 Go 1.22 修复所有闭包问题:用
=复用外层变量仍共享,元素指向同一对象仍可能竞争。 - 循环内 defer,期待每轮执行:defer 在当前函数返回时执行。
- 每轮无界启动 goroutine:需要并发上限和取消协议。
- 迭代器忽略
yield的 false:调用方 break 后必须立即停止。 - 用整数 range 表达任意区间:它只产生
[0,n)。
18. 速查表
for i := 0; i < n; i++ {}
for condition {}
for {}
for i, value := range slice {}
for i := range slice {}
for _, value := range slice {}
for key, value := range mapping {}
for key := range mapping {}
for byteIndex, r := range text {}
for value := range channel {}
for i := range n {}
for value := range sequence {}控制流:
break // 结束最内层 for/switch/select
continue // 下一轮最内层 for
break Outer // 结束带标签的外层语句
continue Outer // 继续带标签的外层 for
return // 结束函数,执行 defer修改 slice 元素:
for i := range values {
values[i] = transform(values[i])
}稳定 map 输出:
keys := slices.Sorted(maps.Keys(m))
for _, key := range keys {
fmt.Println(key, m[key])
}maps.Keys 在现代标准库中返回迭代序列,slices.Sorted 收集并排序它。
19. 练习
- 不使用额外 slice,原地反转
[]int。分别写三段式双指针版本和单索引版本。 - 统计 UTF-8 字符串的字节数、rune 数,并打印每个 rune 的字节起始位置。加入非法 UTF-8 测试。
- 实现稳定输出
map[string]int的函数;解释排序使复杂度从 O(n) 变成什么。 - 实现
FilterInPlace[T any],复用输入 slice 的底层数组。说明返回 slice 与原 slice 的别名关系,以及是否需要清理尾部引用。 - 写一个 context 可取消的生产者,并让消费者在收到 5 个值后提前退出。验证生产者不会泄漏。
- 编写
Range(start, end, step int) iter.Seq[int],处理正步长、负步长和 step 为零;调用方 break 后不再生成值。 - 编写
Zip[A, B any],用iter.Pull合并两个 Seq,在较短序列结束时停止,并保证两个stop都执行。 - 写一个测试演示
for _, v = range values仍复用外层变量,再改成:=,说明 Go 1.22 语义差别。 - 对
[]LargeStruct比较按值 range、按索引取指针和按索引取值的 benchmark,使用-benchmem,不要只凭一次结果下结论。 - 把一个循环内
defer file.Close()的程序重构成单次处理函数,用较低文件描述符限制验证改造效果。
20. 可运行示例
本节的程序位于 examples/ch06,分别验证 range 元素副本、整数迭代和函数迭代器的停止协议。
20.1 区分 range 值副本与原元素
先区分元素副本与循环变量。 为什么修改 range 的 value 没有效果?闭包捕获循环变量在现代 Go 中是什么行为?
package main
import "fmt"
func main() {
numbers := []int{10, 20, 30}
for _, value := range numbers {
// value 是元素副本;修改它不会改回 slice。
value *= 10
}
fmt.Printf("修改 value 后: %v\n", numbers)
for index := range numbers {
// 需要修改原元素时,用索引回到 slice 的存储位置。
numbers[index] *= 10
}
fmt.Printf("按索引修改后: %v\n", numbers)
// Go 1.22 起,使用 := 声明的 range 变量每轮都是新的变量,
// 因而每个闭包会记住当轮的 value,而不是共享最后一个值。
var readers []func() int
for _, value := range numbers {
readers = append(readers, func() int { return value })
}
fmt.Printf("闭包结果: %d %d %d\n", readers[0](), readers[1](), readers[2]())
}运行:
go run ./examples/ch06/range-values预期输出:
修改 value 后: [10 20 30]
按索引修改后: [100 200 300]
闭包结果: 100 200 300两个行为分别由两条规则决定。
for _, value := range numbers把元素复制给 value,对 value 乘十只改了副本。for index := range numbers再通过numbers[index]定位原存储,修改才会保留下来。- Go 1.22 起,用
:=声明的 range 变量每轮都有新的实例,三个闭包各自捕获当轮值。 - 如果改成复用循环外的变量,例如
var value int; for _, value = range numbers,闭包仍会共享同一个变量。
切换声明方式再运行。 把第二段改回按值 range,确认原切片不变;再按上面的 = 方式复用外部变量,观察三个闭包都读到什么。
20.2 range 整数完成固定次数迭代
固定次数循环可以直接 range 整数。 只想执行 n 次时,怎样避免手写初始化、条件和递增表达式?
package main
import "fmt"
func main() {
const count = 5
// 从 Go 1.22 开始可以 range 整数,依次产生 0 到 count-1。
// 它不会产生 count,也不能直接表达负步长。
sum := 0
for index := range count {
fmt.Printf("%d ", index)
sum += index
}
fmt.Printf("\nsum=%d\n", sum)
}运行:
go run ./examples/ch06/integer-range预期输出:
0 1 2 3 4
sum=10边界和类型都来自 range 表达式。
range count依次产生 0 到count-1,总共 count 次。- count 为 0 或负数时不会进入循环;它不是包含终点的数学闭区间。
- 迭代变量的类型由 range 表达式决定;这里无类型常量根据变量上下文得到
int。 - 整数 range 适合“做 n 次”,但不能直接表达起点、步长或倒序,这些情况三段式 for 更清楚。
换几个 count 观察边界。 把 count 改成 0 和 1;再声明 var count uint8 = 5,用 %T 打印 index 的类型。
20.3 让迭代器响应调用方 break
yield 的返回值就是停止信号。 自定义 iter.Seq 怎样知道消费者已经提前停止,避免继续生产或泄漏资源?
package main
import (
"fmt"
"iter"
)
func countUp(limit int) iter.Seq[int] {
return func(yield func(int) bool) {
// 整数 range 决定候选值,真正的迭代边界还要受消费者控制。
for value := range limit {
fmt.Printf("produce %d\n", value)
// yield 返回 false,表示调用方已经 break;生产者必须立刻停止。
if !yield(value) {
fmt.Println("producer stopped")
return
}
}
}
}
func main() {
for value := range countUp(10) {
fmt.Printf("consume %d\n", value)
if value == 2 {
break
}
}
}运行:
go run ./examples/ch06/iterator-stop预期输出:
produce 0
consume 0
produce 1
consume 1
produce 2
consume 2
producer stopped生产者与消费者通过布尔值完成握手。
iter.Seq[int]本质上是接收yield func(int) bool的函数,生产者通过调用 yield 交出一个值。- range 循环继续时 yield 返回 true;调用方在 2 处 break 后,当前 yield 返回 false。
- 生产者必须立即 return。真实迭代器应在 return 前关闭文件、释放锁或停止后台工作。
- 输出中没有
produce 3,这正是停止信号被正确传播的可见证据。
故意忽略一次停止信号。 删除 if !yield(value) { return } 中的 return,观察生产者为什么继续工作;再把 break 条件改成 0,验证第一项后即可停止。
21. 官方资料
- Go 语言规范:For statements
- Go 语言规范:For statements with range clause
- Go 语言规范:Break statements
- Go 语言规范:Continue statements
- Go 语言规范:Labeled statements
- Go 语言规范:Goto statements
- Go 1.22 Release Notes
- Fixing For Loops in Go 1.22
- Go 1.23 Release Notes
iter包slices包maps包- Go 1.26 Release Notes
写循环之前,先确认数据源的迭代协议:range 变量是不是副本,顺序是否稳定,提前退出又怎样传回数据源。把这三件事弄清楚,比选择哪种 for 看起来更简洁重要得多。