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

Go ascii85.NewEncoder Flush 后为什么还要 Close

来源:17golang原创

时间:2026-11-10 19:13:19 245浏览 收藏

在 Go 里把一段二进制数据流写成 ASCII85 文本时,ascii85.NewEncoder 和外层 bufio.Writer 可能各自留着一部分数据。只调用外层的 Flush 并不能替编码器完成最后一块;正确收尾是先关闭 ASCII85 编码器,再刷新外层写缓冲。原因很具体:ASCII85 按 4 个输入字节组成一个 32 位块,尾部不足 4 字节时要等 Close 才会编码写出。

官方文档:https://pkg.go.dev/encoding/ascii85#NewEncoder

要点速览
  • NewEncoder 返回的是 io.WriteCloser,不是只需要 Write 的普通 Writer。
  • Close 负责清空 ASCII85 编码器自己的尾部缓冲,外层 Flush 只负责把已经写入外层 Writer 的内容推出去。
  • 嵌套写入器的收尾顺序是 encoder.Close(),然后 buffer.Flush(),两个错误都要处理。

先看清楚两层缓冲各自保存什么

这次问题通常出现在“写入没有报错,但结果末尾缺几个字符”的场景。NewEncoder(w) 会把输入交给 ASCII85 编码器;编码器内部用 4 字节缓冲等待一个完整块,已经完成的块才会写入 w。如果 w 又是 bufio.Writer,那么编码结果还会在外层继续暂存。

因此,bufio.Writer.Flush() 看到的只是“已经从 ASCII85 编码器写进来的内容”。它不知道编码器内部是否还握着 1~3 个原始字节,也不会主动调用编码器的 Close。

ASCII85 编码器的四字节尾部缓冲、外层 bufio.Writer 与最终 Writer 的静态关系说明图
图1:静态结构说明图,查看 ASCII85 编码器与外层写缓冲的两层边界;这不是运行截图。

旧写法的风险:Flush 只刷新了外层

下面的写法把两个动作混在一起了:

var dst bytes.Buffer
buf := bufio.NewWriter(&dst)
enc := ascii85.NewEncoder(buf)

// 写入 3 个字节时,编码器仍可能把它们留在自己的尾部缓冲中。
_, err := enc.Write([]byte("abc"))
if err != nil {
    return err
}

// 这里只能刷新已经到达 buf 的编码结果,不能替 enc 补齐尾块。
return buf.Flush()

如果输入长度刚好是 4 的倍数,这个问题可能暂时不显眼,因为完整块已经编码;一旦最后一段只有 1~3 个字节,就会出现截断。这个偶尔“看起来正常”的特征,正是它容易在回归测试中漏掉的原因。

正确写法:先 Close,再 Flush

Close 的语义不是关闭底层文件或网络连接,而是结束这个 ASCII85 编码器并写出待处理的尾部。标准库实现会在 Close 中检查内部缓冲;如果还有剩余字节,就用短块规则编码后写到传入的 Writer。之后才轮到外层 bufio.Writer.Flush 把这些新写入的字符送到最终 Writer。

func writeASCII85(dst io.Writer, src []byte) error {
    // 外层缓冲只负责暂存已经编码好的文本。
    buf := bufio.NewWriter(dst)
    enc := ascii85.NewEncoder(buf)

    // Write 可能只把最后 1~3 个原始字节留在 enc 内部。
    if _, err := enc.Write(src); err != nil {
        return err
    }

    // Close 先清掉 ASCII85 编码器自己的尾部缓冲。
    if err := enc.Close(); err != nil {
        return err
    }

    // Close 新写入 buf 的内容还在外层,因此最后再 Flush。
    return buf.Flush()
}

如果把 Flush 放在 Close 前面,之后至少还需要再次 Flush 才能把 Close 产生的尾部送出去;直接采用“先内层收尾、再外层刷新”的顺序更不容易遗漏。

ASCII85 Write、nbuf、Close、Flush、错误返回和禁止再次 Write 的生命周期边界结构图
图2:收尾边界结构图,查看 Close、Flush、错误返回与生命周期约束;这不是运行截图。

收尾时还要处理哪些边界

场景应该怎么做原因
尾部不足 4 字节必须调用 enc.Close()让编码器输出最后的短块
外层是 bufio.WriterClose 成功后再调用 Flush()把 Close 新写入的文本推出去
Close 返回错误立即返回或记录错误,不要忽略底层 Writer 可能已经拒绝收尾写入
Close 之后继续 Write视为生命周期错误文档明确不允许在 Close 后继续写

这里的 Close 只结束 ASCII85 编码器本身。若最终 Writer 代表文件、压缩器或网络响应,它们各自仍可能有自己的收尾协议,不能因为 ASCII85 已经关闭就推断整个资源链都已经完成。

回归检查和收尾清单

修复后至少覆盖三组输入:长度刚好是 4 的倍数、长度多出 1~3 个字节、空输入。重点不是比较某一串固定文本,而是确认外层 Writer 在 Close 之后仍能收到尾块,并且 Close 或 Flush 的错误不会被吞掉。

  • 把 ascii85.NewEncoder 当作 io.WriteCloser 管理,不把它当普通 io.Writer。
  • 嵌套缓冲时遵循从内到外的收尾顺序:编码器 Close,外层 Writer Flush。
  • 专门测试最后不足一个 4 字节块的输入,避免“整块输入通过、尾部输入失败”。
  • Close 后不再 Write;如果下游还有关闭动作,继续按各层资源的生命周期处理。

常见问题

ascii85.NewEncoder 有公开的 Flush 方法吗?

没有。它通过返回的 io.WriteCloser 暴露 Close,由 Close 完成编码器尾部收尾;外层 Writer 是否有 Flush 是另一层接口的事情。

为什么先 Flush 再 Close 有时也能得到完整结果?

当输入刚好没有待处理尾块,或者最终 Writer 没有继续缓存时,错误顺序可能暂时不暴露。但只要 Close 产生了新输出,之前的 Flush 已经结束,仍可能留下未送出的内容。

Close 成功后还要不要 Flush?

如果传给 NewEncoder 的就是 bufio.Writer,一般要。Close 只是把尾部写入这个 Writer,Flush 才负责把外层缓冲送到最终目标。

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