Skip to content

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 负责从某种数据源取得下一组迭代值。

基础形式:

go
for init; condition; post {
	body
}

省略 initpost,就是条件循环:

go
for condition {
	body
}

连条件也省略,就是无限循环:

go
for {
	body
}

range 形式:

go
for index, value := range source {
	body
}

source 不只可以是容器。截至 Go 1.26,range 支持:

range 对象第一迭代值第二迭代值
array / *array索引元素副本
slice索引元素副本
stringUTF-8 字节索引rune
map
channel接收到的元素
integer从 0 开始的整数
func(func() bool)
func(func(V) bool)V
func(func(K, V) bool)KV

沿着这张表继续看,许多边界最终都可以归结为三个问题:

  1. 每次拿到的是原值还是副本?
  2. 迭代顺序是否有保证?
  3. 循环怎样得知数据结束,提前停止又怎样通知数据源?

2. 三种基本 for

2.1 三段式

go
for i := 0; i < 10; i++ {
	fmt.Println(i)
}

执行顺序是:

  1. i := 0 只执行一次;
  2. 每轮开始检查 i < 10
  3. 条件为真时执行循环体;
  4. 执行 i++
  5. 回到步骤 2。

与 Java 不同,这三部分不加圆括号。初始化和后置部分必须是简单语句,不能写多个用逗号分隔的表达式;需要同时更新多个值时,可以使用平行赋值:

go
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,直接省略前后两段:

go
scanner := bufio.NewScanner(r)
for scanner.Scan() {
	process(scanner.Text())
}
if err := scanner.Err(); err != nil {
	return err
}

条件在每轮开始时都会重新求值,因此可以直接放置会推进状态的函数调用,例如 scanner.Scan() 或解析器的 Next()

2.3 无限循环

go
for {
	conn, err := listener.Accept()
	if err != nil {
		return err
	}
	go handle(conn)
}

无限循环通常依靠 breakreturn、context 取消、channel 关闭或错误结束。生产服务不能只假设它会“永远运行”,对应的关闭路径也要一并设计。

3. 初始化、条件与后置语句

3.1 三部分都可以省略

go
for ; i < limit; {
	i += step
}

上面合法,但通常写成更自然的条件循环:

go
for i < limit {
	i += step
}

仅保留分号而省略条件,条件视为 true

go
for i := 0; ; i++ {
	if done(i) {
		break
	}
}

3.2 ++-- 是语句

Go 的 i++i-- 没有表达式值:

go
i++
// j := i++ // 编译错误

它们只能作为语句,不能嵌入函数参数或复杂表达式,也没有前置 ++i。这减少了一个表达式中同时读写变量的歧义。

3.3 作用域

初始化语句声明的变量只在 for 语句内可见:

go
for i := 0; i < 3; i++ {
	fmt.Println(i)
}
// fmt.Println(i) // 编译错误

如果循环后还要用计数器,先在外层声明:

go
i := 0
for ; i < len(values) && valid(values[i]); i++ {
	// ...
}
fmt.Println("停在:", i)

4. break、continue、return 与 goto

4.1 break

不带标签的 break 终止最内层 forswitchselect

go
for _, value := range values {
	if value < 0 {
		break
	}
	fmt.Println(value)
}

4.2 continue

continue 跳过本轮剩余部分:

go
for _, path := range paths {
	if strings.TrimSpace(path) == "" {
		continue
	}
	if err := process(path); err != nil {
		return err
	}
}

在三段式循环里,continue 之后仍会执行后置语句,再检查条件。

4.3 return

return 结束整个函数,并执行该函数已经注册的 defer:

go
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,但它不能跳过变量声明进入该变量的作用域,也不能跳入另一个块。少数资源清理或生成代码可能用到,常规业务循环优先使用函数提取、breakcontinuereturn

go
for {
	if fatal() {
		goto Done
	}
}

Done:
cleanup()

如果标签只是为了统一清理,defer cleanup() 往往更可靠。

5. 标签与多层控制流

标签是标识符加冒号,可以作为 breakcontinue 的目标。

5.1 跳出多层循环

go
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 外层循环

go
Rows:
for _, row := range rows {
	for _, value := range row {
		if value < 0 {
			continue Rows
		}
	}
	accept(row)
}

标签应紧邻目标语句,名字表达意图,如 RowsSearchRetry。标签过多通常说明一个函数承担了太多阶段,应考虑提取函数。

5.3 switch / select 嵌在循环中

这是标签最有实际价值的场景:

go
Loop:
for {
	select {
	case value, ok := <-ch:
		if !ok {
			break Loop
		}
		use(value)
	case <-ctx.Done():
		return ctx.Err()
	}
}
return nil

break 不带标签只会离开 select,下一轮 for 仍会继续。

6. range 的统一规则

range 表达式通常在进入循环前求值一次:

go
for i := range makeSlice() {
	fmt.Println(i)
}

makeSlice() 只调用一次,不会在每轮重新调用。规范还规定了一个优化例外:如果最多只有一个迭代变量,并且 len(rangeExpression) 是常量,range 表达式可能完全不求值。

迭代变量有三种写法:

go
for i, value := range values { // 每轮声明/赋值
	_, _ = i, value
}

var i int
var value Item
for i, value = range values { // 写入预先存在的变量
	_, _ = i, value
}

for range values { // 不需要任何迭代值
	do()
}

只要第一个值:

go
for i := range values {
	fmt.Println(i)
}

只要第二个值时用空白标识符:

go
for _, value := range values {
	fmt.Println(value)
}

末尾的空白标识符等价于省略:

go
for key, _ = range m {}
// 等价于
for key = range m {}

7. 数组与切片

7.1 索引和值

go
values := []string{"a", "b", "c"}
for i, value := range values {
	fmt.Println(i, value)
}

value 是元素的副本。修改它不会修改 slice:

go
numbers := []int{1, 2, 3}
for _, number := range numbers {
	number *= 10
}
fmt.Println(numbers) // [1 2 3]

按索引修改:

go
for i := range numbers {
	numbers[i] *= 10
}

若元素是指针,复制的是指针值,通过指针修改目标仍然可见:

go
for _, user := range users { // users 是 []*User
	user.Active = true
}

7.2 大 struct 的复制成本

go
type Record struct {
	ID      uint64
	Payload [4096]byte
}

for _, record := range records {
	consume(record)
}

每轮会取得元素值。编译器可能优化部分复制,但不能靠猜测保证。若只读大对象,可以按索引取地址:

go
for i := range records {
	consumePtr(&records[i])
}

是否更快要 benchmark;指针会增加别名和逃逸可能,API 语义应优先。

7.3 数组表达式可能整体复制

数组是值类型。range 一个数组值时,range 表达式会被求值并保存,可能复制整个数组:

go
var array [1024]Record
for i := range array {
	_ = i
}

只使用索引且数组长度为常量时,规范允许不求值 range 表达式;一旦取第二值,数组的值语义便不可忽略。遍历大数组时可以 range 指针:

go
for i, value := range &array {
	_, _ = i, value
}

7.4 循环中 append

slice 的 range 在开始时确定要遍历的长度。循环里 append 不会让当前循环自动扩展:

go
values := []int{1, 2, 3}
for _, value := range values {
	values = append(values, value)
}
fmt.Println(len(values)) // 6,循环只执行原来的 3 次

如果 append 没触发扩容,修改共享底层数组可能影响后续尚未读取的元素;触发扩容后又是另一种行为。不要写依赖容量偶然值的逻辑。需要队列式“边遍历边增长”时,使用显式索引条件:

go
for i := 0; i < len(queue); i++ {
	queue = append(queue, expand(queue[i])...)
}

同时必须有终止条件,避免无界增长。

8. 字符串与 Unicode

range string 按 UTF-8 解码,每轮得到:

  • 当前 rune 的起始字节索引;
  • 解码后的 rune。
go
text := "Go语言"
for index, r := range text {
	fmt.Printf("byte=%d rune=%c U+%04X\n", index, r, r)
}

索引不是“第几个字符”,而是字节偏移。len(text) 也是字节数:

go
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

go
if !utf8.ValidString(text) {
	return errors.New("文本不是合法 UTF-8")
}

rune 也不等于用户看到的“字符”。组合音标、emoji 和旗帜可能由多个 code point 组成。标准库没有直接提供完整的 grapheme cluster 分割;界面长度需求要采用符合 Unicode 文本边界的专门方案。

只需要 rune 而不需要索引:

go
for _, r := range text {
	fmt.Println(r)
}

需要逐字节:

go
for i := 0; i < len(text); i++ {
	fmt.Printf("%02x\n", text[i])
}

9. map

9.1 顺序不确定

go
for key, value := range settings {
	fmt.Println(key, value)
}

map 的迭代顺序未指定,不保证每次相同,也不能依赖插入顺序。稳定输出需要收集并排序键:

go
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、因迭代而不同。

因此“遍历并删除不满足条件的键”是可定义的:

go
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 报告的竞争也不能留着:

bash
go test -race ./...

10. channel

对 channel 做 range 只有一个迭代值:

go
for value := range ch {
	process(value)
}

循环不断接收,直到 channel 被关闭并且缓冲区已排空。它不会因为暂时没有值就结束。

两个重要边界:

  • nil channel 上 range 永久阻塞;
  • 没有人关闭的 channel 在生产者退出后会让消费者永久等待。

典型生产者:

go
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 整数:

go
for i := range 5 {
	fmt.Println(i) // 0 1 2 3 4
}

若整数 n <= 0,循环执行零次:

go
for range -3 {
	panic("不会执行")
}

迭代值从 0 开始,逐次加 1,到 n-1 为止。类型规则要区分是否预先声明变量:

go
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,仍用三段式:

go
for i := 10; i <= 20; i += 2 {
	fmt.Println(i)
}

不要为了使用新语法把清晰的集合遍历改成下标 range。读取 slice 元素时,for i, value := range values 仍更直接。

12. 函数迭代器

Go 1.23 起,range 可以消费特定函数签名:

go
func(func() bool)
func(func(V) bool)
func(func(K, V) bool)

标准库 iter 包给它们命名:

go
type Seq[V any] func(yield func(V) bool)
type Seq2[K, V any] func(yield func(K, V) bool)

12.1 编写 Seq

go
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 接收值。若循环体执行 breakyield 返回 false,迭代器必须停止,不得再次调用它。

12.2 continue、break 与 return

go
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

go
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 形式:

go
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 旧问题

旧语义下,使用 := 声明的循环变量在所有迭代间复用,闭包可能都观察到最后一个值:

go
for _, value := range values {
	go func() {
		fmt.Println(value)
	}()
}

Go 1.22 的新语义让每次迭代拥有新的循环变量。对于语言版本为 Go 1.22 及以上的模块,上面的每个闭包捕获对应迭代的 value

13.2 不只影响 goroutine

保存地址或普通闭包也受影响:

go
var functions []func() string
for _, value := range []string{"a", "b", "c"} {
	functions = append(functions, func() string { return value })
}

新语义得到 a、b、c。它不仅是并发修复,而是变量身份发生变化。

13.3 = 形式仍复用已有变量

新语义针对循环声明的变量。若使用赋值形式写入循环外的变量,它仍是同一个变量:

go
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.modgo 行,不只是安装了哪版工具链。维护旧模块时运行:

bash
go version
go env GOMOD
go vet ./...

升级 go 行前应执行测试,尤其关注过去依赖循环变量复用的罕见代码。value := value 这类旧式防捕获写法在新版本中通常冗余,但保留它一般不改变正确性。

14. 修改、删除与并发边界

14.1 range 值是副本

[]struct

go
for _, user := range users {
	user.Active = true // 只改副本
}

应写:

go
for i := range users {
	users[i].Active = true
}

map[K]Struct,map 元素不可寻址,通常取出、修改、写回:

go
for key, user := range users {
	user.Active = true
	users[key] = user
}

或者 map 保存指针,但那会改变共享、nil、并发和所有权语义,不能只为了少一行赋值就改数据模型。

14.2 slice range 与并发

一个 goroutine range slice,另一个 goroutine append 或修改同一底层数组,通常是数据竞争。slice header 复制不代表底层数组隔离。需要锁、不可变快照或所有权转移:

go
mu.RLock()
snapshot := slices.Clone(values)
mu.RUnlock()

for _, value := range snapshot {
	use(value)
}

克隆有分配和复制成本,但把锁持有时间与慢处理解耦。是否合适取决于数据量和一致性要求。

14.3 闭包并发还要看元素本身

Go 1.22 解决了循环变量身份问题,没有自动解决被指向对象的数据竞争:

go
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 验证:

bash
go test -bench=. -benchmem ./...
go test -run=^$ -bench=BenchmarkLoop -memprofile=mem.out
go tool pprof mem.out

15.2 预分配

知道输出上限时预分配:

go
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 在外围函数返回时执行,不在每次迭代结束时执行:

go
for _, path := range paths {
	file, err := os.Open(path)
	if err != nil {
		return err
	}
	defer file.Close() // 所有文件都要等外层函数结束
}

提取单次处理函数:

go
func processFile(path string) error {
	file, err := os.Open(path)
	if err != nil {
		return err
	}
	defer file.Close()
	return consume(file)
}

这样每轮函数返回就关闭资源,错误路径也不会漏。

15.4 并发循环要有上限

go
for _, item := range items {
	go process(item)
}

输入很大时会创建同等数量 goroutine。使用固定 worker、信号量或任务组限制并发,并定义错误取消、结果收集和 panic 策略。循环变量已安全不等于并发模型已安全。

16. 与 Java 的对照

JavaGo说明
for (init; cond; post)for init; cond; postGo 无圆括号
while (cond)for condGo 无 while 关键字
for (;;)for {}无限循环
enhanced forfor _, v := range valuesGo 常同时提供索引
break labelbreak LabelGo 标签写在目标语句前
continue labelcontinue Label用于继续外层循环
Iterator<T>iter.Seq[T] / range funcGo 1.23 的 push 迭代协议
IntStream.range(0, n)range nGo 1.22,零到 n-1
String.codePoints()range stringGo 返回字节索引和 rune
Map 遍历range map顺序未指定,修改规则不同

Java enhanced for 的局部变量也是元素值;Go 对 slice struct 元素的 range 变量同样不是“元素引用”。两边都不应通过给循环变量重新赋值来期待修改容器,但 Java 对对象字段的修改与 Go 的指针/值选择需要分别分析。

17. 常见误区

  • 以为 Go 有 whiledo/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. 速查表

go
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 {}

控制流:

go
break          // 结束最内层 for/switch/select
continue       // 下一轮最内层 for
break Outer    // 结束带标签的外层语句
continue Outer // 继续带标签的外层 for
return         // 结束函数,执行 defer

修改 slice 元素:

go
for i := range values {
	values[i] = transform(values[i])
}

稳定 map 输出:

go
keys := slices.Sorted(maps.Keys(m))
for _, key := range keys {
	fmt.Println(key, m[key])
}

maps.Keys 在现代标准库中返回迭代序列,slices.Sorted 收集并排序它。

19. 练习

  1. 不使用额外 slice,原地反转 []int。分别写三段式双指针版本和单索引版本。
  2. 统计 UTF-8 字符串的字节数、rune 数,并打印每个 rune 的字节起始位置。加入非法 UTF-8 测试。
  3. 实现稳定输出 map[string]int 的函数;解释排序使复杂度从 O(n) 变成什么。
  4. 实现 FilterInPlace[T any],复用输入 slice 的底层数组。说明返回 slice 与原 slice 的别名关系,以及是否需要清理尾部引用。
  5. 写一个 context 可取消的生产者,并让消费者在收到 5 个值后提前退出。验证生产者不会泄漏。
  6. 编写 Range(start, end, step int) iter.Seq[int],处理正步长、负步长和 step 为零;调用方 break 后不再生成值。
  7. 编写 Zip[A, B any],用 iter.Pull 合并两个 Seq,在较短序列结束时停止,并保证两个 stop 都执行。
  8. 写一个测试演示 for _, v = range values 仍复用外层变量,再改成 :=,说明 Go 1.22 语义差别。
  9. []LargeStruct 比较按值 range、按索引取指针和按索引取值的 benchmark,使用 -benchmem,不要只凭一次结果下结论。
  10. 把一个循环内 defer file.Close() 的程序重构成单次处理函数,用较低文件描述符限制验证改造效果。

20. 可运行示例

本节的程序位于 examples/ch06,分别验证 range 元素副本、整数迭代和函数迭代器的停止协议。

20.1 区分 range 值副本与原元素

先区分元素副本与循环变量。 为什么修改 range 的 value 没有效果?闭包捕获循环变量在现代 Go 中是什么行为?

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]())
}

运行:

bash
go run ./examples/ch06/range-values

预期输出:

text
修改 value 后: [10 20 30]
按索引修改后: [100 200 300]
闭包结果: 100 200 300

两个行为分别由两条规则决定。

  1. for _, value := range numbers 把元素复制给 value,对 value 乘十只改了副本。
  2. for index := range numbers 再通过 numbers[index] 定位原存储,修改才会保留下来。
  3. Go 1.22 起,用 := 声明的 range 变量每轮都有新的实例,三个闭包各自捕获当轮值。
  4. 如果改成复用循环外的变量,例如 var value int; for _, value = range numbers,闭包仍会共享同一个变量。

切换声明方式再运行。 把第二段改回按值 range,确认原切片不变;再按上面的 = 方式复用外部变量,观察三个闭包都读到什么。

20.2 range 整数完成固定次数迭代

固定次数循环可以直接 range 整数。 只想执行 n 次时,怎样避免手写初始化、条件和递增表达式?

go
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)
}

运行:

bash
go run ./examples/ch06/integer-range

预期输出:

text
0 1 2 3 4
sum=10

边界和类型都来自 range 表达式。

  1. range count 依次产生 0 到 count-1,总共 count 次。
  2. count 为 0 或负数时不会进入循环;它不是包含终点的数学闭区间。
  3. 迭代变量的类型由 range 表达式决定;这里无类型常量根据变量上下文得到 int
  4. 整数 range 适合“做 n 次”,但不能直接表达起点、步长或倒序,这些情况三段式 for 更清楚。

换几个 count 观察边界。 把 count 改成 0 和 1;再声明 var count uint8 = 5,用 %T 打印 index 的类型。

20.3 让迭代器响应调用方 break

yield 的返回值就是停止信号。 自定义 iter.Seq 怎样知道消费者已经提前停止,避免继续生产或泄漏资源?

go
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
		}
	}
}

运行:

bash
go run ./examples/ch06/iterator-stop

预期输出:

text
produce 0
consume 0
produce 1
consume 1
produce 2
consume 2
producer stopped

生产者与消费者通过布尔值完成握手。

  1. iter.Seq[int] 本质上是接收 yield func(int) bool 的函数,生产者通过调用 yield 交出一个值。
  2. range 循环继续时 yield 返回 true;调用方在 2 处 break 后,当前 yield 返回 false。
  3. 生产者必须立即 return。真实迭代器应在 return 前关闭文件、释放锁或停止后台工作。
  4. 输出中没有 produce 3,这正是停止信号被正确传播的可见证据。

故意忽略一次停止信号。 删除 if !yield(value) { return } 中的 return,观察生产者为什么继续工作;再把 break 条件改成 0,验证第一项后即可停止。

21. 官方资料

写循环之前,先确认数据源的迭代协议:range 变量是不是副本,顺序是否稳定,提前退出又怎样传回数据源。把这三件事弄清楚,比选择哪种 for 看起来更简洁重要得多。

以 Go 官方规范与标准库文档为准,示例面向 Go 1.26。