登录
推荐 文章 Go 技术 课程 下载 专题 AI
首页 >  Golang >  Go教程

Go 迭代器提前停止时怎么释放资源

来源:17golang原创

时间:2026-10-06 09:20:25 359浏览 收藏

Go 迭代器提前停止时,资源能否释放,取决于资源生命周期放在哪里。最短结论是:push 迭代器把关闭动作写在迭代器函数内部,并在 yield 返回 false 后立即 return;使用 iter.Pull 时,拿到 stop 后立刻 defer stop()。

先看官方约定

iter 包文档定义了 Seq 和 Seq2。迭代器在序列结束,或 yield 返回 false 时停止。for range 中执行 break,会让对应的 yield 调用返回 false。迭代器实现必须接住这个信号,不能继续调用 yield。

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

真正需要保护的资产通常不是切片元素,而是文件句柄、数据库游标、网络响应体、锁以及为 pull 适配启动的后台 goroutine。风险来自“调用方提前结束,但生产方仍以为会消费到末尾”。

Go push 迭代器的资源边界与提前停止关系图

图1:文件资源、Seq2 迭代器、yield 与 range 消费者的静态边界关系,不是运行截图。

push 迭代器:让 defer 跟着迭代器返回

下面的文件行迭代器把打开、读取和关闭都放进同一个迭代器函数。调用方无论正常读完、遇到错误,还是主动 break,最终都会离开该函数并执行 defer f.Close()。

package lines

import (
    "bufio"
    "iter"
    "os"
)

// Lines 按行读取文件,并把读取错误作为第二个值交给调用方。
func Lines(path string) iter.Seq2[string, error] {
    return func(yield func(string, error) bool) {
        // 资源在迭代真正开始时创建。
        f, err := os.Open(path)
        if err != nil {
            // 打开失败只报告一次,然后结束序列。
            yield("", err)
            return
        }
        // 正常耗尽、错误退出和提前停止都会执行关闭。
        defer f.Close()

        scanner := bufio.NewScanner(f)
        for scanner.Scan() {
            // false 表示调用方已经不需要后续值。
            if !yield(scanner.Text(), nil) {
                return
            }
        }
        if err := scanner.Err(); err != nil {
            // 扫描错误作为最后一个结果交给调用方。
            yield("", err)
        }
    }
}

调用方只处理需要的部分即可:

for line, err := range Lines("access.log") {
    if err != nil {
        // 收到读取错误后停止,迭代器内部仍会关闭文件。
        log.Printf("read lines: %v", err)
        break
    }
    if strings.Contains(line, "READY") {
        // break 会让当前 yield 返回 false。
        break
    }
}

最危险的写法:忽略 yield 的返回值

下面的包装器在消费者停止后仍继续生产值。官方文档还明确指出,在 yield 已返回 false 后再次调用它会触发 panic。

func BadMap(seq iter.Seq[string]) iter.Seq[string] {
    return func(yield func(string) bool) {
        for v := range seq {
            // 错误:丢失了停止信号。
            yield(strings.ToUpper(v))
        }
    }
}

正确做法是逐层传播 false:

func MapUpper(seq iter.Seq[string]) iter.Seq[string] {
    return func(yield func(string) bool) {
        for v := range seq {
            // 调用方停止时,包装器也立即返回。
            if !yield(strings.ToUpper(v)) {
                return
            }
        }
    }
}

pull 迭代器:stop 是调用方持有的关闭句柄

iter.Pull 把 push 序列转换成 next 与 stop。只要 next 还没有返回 false,调用方决定不再取值时就必须调用 stop。最稳妥的习惯是在转换成功后立即 defer stop()。

next, stop := iter.Pull(source)
// 即使中途 return 或 break,也能通知迭代器结束。
defer stop()

for {
    v, ok := next()
    if !ok {
        // 序列已经自然耗尽。
        break
    }
    if enough(v) {
        // defer stop 会结束尚未耗尽的迭代。
        break
    }
    use(v)
}

iter.Pull 中 next stop 与后台迭代器的资源所有权关系图

图2:iter.Pull、next、stop、后台迭代器和消费者之间的静态所有权关系。

官方说明还给出了两个边界:stop 可以重复调用,序列已经结束后调用也安全;但 next 与 stop 不能由多个 goroutine 同时调用。

风险分级与防护控制

风险后果控制方式
push 迭代器忽略 falsepanic、无效计算、资源延迟释放每次调用 yield 都检查返回值并立即 return
关闭动作放在调用方包装器或复用者容易忘记关闭资源由迭代器创建,就在迭代器内部 defer 关闭
Pull 后忘记 stop后台迭代器和 goroutine 不能退出获得 stop 后立即 defer stop()
多层适配器吞掉停止信号上游继续读文件或连接每层都把 false 原样传播
并发调用 next 与 stop违反 API 约定,行为不可依赖限定在同一控制流中串行调用

审计时重点看什么

  • 资源在哪里创建,关闭责任是否在同一个词法作用域内。
  • 所有 yield(...) 是否都检查了布尔返回值。
  • 包装器收到 false 后是否立即返回,而不是继续循环。
  • 每处 iter.Pull 或 iter.Pull2 后是否都有 defer stop()。
  • 读取错误能否通过 Seq2[value, error] 或其他明确通道到达调用方。

用计数型假资源验证

不要只测试“能拿到几个值”,还要断言资源最终归零。下面的测试同时覆盖自然耗尽和提前停止。

func trackedSeq(open *atomic.Int64) iter.Seq[int] {
    return func(yield func(int) bool) {
        // 模拟打开一个受控资源。
        open.Add(1)
        defer open.Add(-1)

        for i := 0; i 

验证清单

  1. 正常消费到末尾,资源计数归零。
  2. 第一个元素后 break,资源计数仍归零。
  3. 消费者函数中途 return,清理仍执行。
  4. 上游读取报错,错误可见且资源关闭。
  5. 多层 Map、Filter 包装后,停止信号仍能到达最上游。
  6. Pull 模式未耗尽时调用 stop,后台迭代器能够返回。

常见问题

只写 defer Close 就够了吗?

还不够。defer 只有在迭代器函数返回后才执行,因此还必须在 yield 返回 false 时立即 return。

序列已经耗尽,还要调用 stop 吗?

不是必须,但调用是安全的。统一写成 defer stop() 最简单,也能覆盖提前退出。

可以让调用方关闭迭代器内部打开的文件吗?

不建议。资源由谁创建就由谁清理,能避免包装器、错误路径和提前停止路径遗漏关闭。

最终可以把规则压缩成一句话:push 看 false,pull 调 stop,资源清理跟着生产者返回。

参考:iter 包文档、Go 官方博客:Range Over Function Types、Go 1.23 发布说明。

声明:本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
相关阅读
更多>
最新阅读
更多>
课程推荐
更多>