Go bufio.Writer Flush 失败时的资源收尾
bufio.Writer.Flush() 返回错误时,正确做法不是立刻结束收尾,而是保存 Flush 错误,并继续关闭底层资源。Flush 负责把内存缓冲区交给底层 io.Writer,Close 负责释放文件描述符或连接;这是两个不同职责,任何一个失败都不能替代另一个。
我第一次认真处理这个问题,是在一个批量导出任务里:主体循环没有报错,文件也能看到,但结尾偶尔缺数据。原因并不神秘——最后一批数据还留在 bufio.Writer 中,真正的磁盘写入错误直到 Flush 才出现,而原代码既忽略了 Flush 返回值,也没有把 Close 错误带回调用方。
官方文档:https://pkg.go.dev/bufio
Flush 失败后,Close 仍然要执行
bufio.Writer 只包装一个 io.Writer。小块数据通常先进入内存缓冲区,所以 WriteString 返回 nil,并不能证明数据已经落到文件、网络连接或其他底层目标。只有缓冲区被写满,或者代码主动调用 Flush,底层写入错误才可能暴露。

这里最重要的判断有三条:
Flush只把当前缓冲数据写给底层 Writer,它不会替你关闭底层文件。- 即使 Flush 失败,底层文件描述符仍然需要 Close;否则长时间运行的进程可能积累资源。
- Close 本身也可能返回错误。对持久化结果敏感的任务,不应把它当成永远成功的清理动作。
三个常见写法为什么不够稳
1. 直接 defer Flush,却不接收错误
func writeBad(path string, data []byte) error {
f, err := os.Create(path)
if err != nil {
return err
}
defer f.Close() // 错误被忽略,调用方不知道关闭是否成功
w := bufio.NewWriter(f)
defer w.Flush() // Flush 的返回值同样被丢弃
_, err = w.Write(data)
return err
}
这个版本看起来简洁,但最关键的两个收尾错误都没有进入返回值。尤其是函数主体写入成功、最后 Flush 失败时,调用方会收到 nil,误以为文件已经完整写好。
2. 先 Close 文件,再 Flush 缓冲区
顺序反过来后,Flush 会尝试向已经关闭的文件写入,失败几乎是必然的。正常收尾应先 Flush,再 Close;但不论 Flush 成功与否,都要尝试 Close。
3. Flush 一报错就 return
if err := w.Flush(); err != nil {
return fmt.Errorf("flush: %w", err) // 这里返回会跳过后续显式 Close
}
if err := f.Close(); err != nil {
return fmt.Errorf("close: %w", err)
}
如果没有独立的 defer 兜底,这段代码在 Flush 失败时不会执行 Close。问题不在“是否应该返回 Flush 错误”,而在返回前没有完成资源释放。
我更常用的写法:显式 Flush,defer 兜底 Close
对普通文件输出,我倾向于让正常路径清楚可见:业务写入成功后显式 Flush;文件关闭由 defer 保证一定执行,同时把 Close 错误与当前返回错误合并。这样早退路径和 Flush 失败路径都不会漏掉文件关闭。
package report
import (
"bufio"
"errors"
"fmt"
"os"
)
func WriteLines(path string, lines []string) (retErr error) {
f, err := os.Create(path)
if err != nil {
return fmt.Errorf("create output: %w", err)
}
// 无论业务写入或 Flush 在哪里失败,都关闭底层文件并保留 Close 错误
defer func() {
if closeErr := f.Close(); closeErr != nil {
retErr = errors.Join(retErr, fmt.Errorf("close output: %w", closeErr))
}
}()
w := bufio.NewWriterSize(f, 64*1024)
for _, line := range lines {
if _, err := w.WriteString(line + "\n"); err != nil {
// Write 已返回错误时,Writer 会锁存错误,不再做无意义的 Flush 重试
return fmt.Errorf("write buffered data: %w", err)
}
}
// Flush 成功只表示缓冲数据已交给底层 Writer,不代表 Close 一定成功
if err := w.Flush(); err != nil {
return fmt.Errorf("flush buffered data: %w", err)
}
return nil
}
errors.Join 会忽略 nil,并保留所有非空错误,适用于 Go 1.20 及以上版本。因此当 Flush 失败、随后 Close 也失败时,调用方能同时看到两个原因;当 Flush 成功但 Close 失败时,函数依然返回 Close 错误。
这段写法没有显式在正常路径调用 Close,是因为 defer 已经承担了关闭职责。若业务要求“Close 成功后才能执行重命名、提交状态”等后续动作,可以把 Close 改成显式调用,并用布尔变量避免 defer 再次关闭。
把写入、Flush 和 Close 错误都带回调用方

实际项目中可以按下面的优先级理解错误:
| 错误来源 | 通常说明 | 仍需执行的动作 |
|---|---|---|
| Write / WriteString | 缓冲区写满时触发底层写入,或输入处理失败 | 关闭底层资源;不要继续向同一个 Writer 写 |
| Flush | 尚未下沉的数据无法完整交给底层 Writer | 保留错误并 Close;不要把 Close 当作补救写入 |
| Close | 释放或最终关闭底层资源失败 | 返回或记录错误;不要盲目重复 Close |
如果项目仍使用 Go 1.19 或更早版本,可以保留第一个业务错误,并把 Close 错误包装进日志或自定义错误类型。不要为了兼容旧版本而直接丢弃 Close 错误。
为什么 Flush 失败后不该原地重试
官方文档明确说明:一旦 bufio.Writer 在写入底层 Writer 时遇到错误,它将不再接受新数据;后续写入和 Flush 都会返回该错误。也就是说,Writer 会“锁存”第一次底层写错误。
这也是我不建议写 for i := 0; i 的原因。重复调用并不会自动恢复磁盘空间、网络连接或底层 Writer 状态,也不会重新获得已经发生不确定性的写入位置。
如果业务确实需要重试,应把重试设计放在更高一层:
- 保留原始业务数据,而不是依赖 bufio 内部尚未写出的字节。
- 关闭失败的底层资源,重新创建文件或连接。
- 从明确的偏移、临时文件或幂等请求边界重新写入。
- 只有在确定数据不会重复或截断时,才把重试结果提交为最终产物。
Writer.Reset(newWriter) 会丢弃未 Flush 的缓冲数据并清除错误状态。它适合复用缓冲对象,不是“无损恢复失败写入”的按钮。没有保留原始数据时调用 Reset,可能把尚未下沉的内容直接丢掉。
用可控失败 Writer 理解错误出现的位置
下面的小型 Writer 只允许写入有限字节,可用于理解“业务 Write 看似成功,错误却在 Flush 才暴露”的现象。它是教学示例,不代表磁盘或网络的全部失败模式。
package main
import (
"bufio"
"errors"
"fmt"
"io"
)
type limitedWriter struct {
remaining int
}
func (w *limitedWriter) Write(p []byte) (int, error) {
// 底层目标容量耗尽后,直接返回一个可识别错误
if w.remaining == 0 {
return 0, errors.New("simulated destination full")
}
n := len(p)
if n > w.remaining {
n = w.remaining
}
w.remaining -= n
if n
这个例子要表达的不是某个固定输出,而是错误边界:缓冲写入成功只代表数据进入了内存;Flush 的返回值才说明剩余数据是否被底层 Writer 接受。
文件、网络和压缩层的收尾差异
写普通文件
先检查业务写入,再检查 bufio.Writer.Flush,最后检查 os.File.Close。若不能接受半成品文件,建议写入同目录临时文件,完成 Flush 和 Close 后再重命名为目标文件。
写网络连接
Flush 失败通常意味着连接状态已经不可靠。仍应关闭连接,但是否重试请求要由上层协议决定;非幂等请求不能仅因为 Flush 失败就自动重发,因为服务端可能已经收到部分或全部数据。
bufio 外还有压缩 Writer
如果结构是 bufio.Writer -> gzip.Writer -> os.File,每一层都有自己的收尾语义。通常应从最外层向内层完成数据终结,再关闭文件:先 Flush bufio,再 Close gzip 以写出压缩尾部,最后 Close 文件。任何中间层失败都不能省略底层资源关闭。
延伸问题
Flush 成功是否等于数据已经持久化到磁盘?
不等于。Flush 只保证数据被写给底层 Writer。对于 os.File,操作系统仍可能缓存数据;确有持久化要求时还要评估 File.Sync、文件系统和存储设备的语义。
可以同时 defer Flush 和 Close 吗?
语法上可以,但必须注意 defer 的后进先出顺序,并且要能收集两个返回错误。相比之下,显式 Flush 加一个负责 Close 的 defer 更直观,也更不容易把错误悄悄丢掉。
Flush 失败后还要读取 Buffered 吗?
Buffered() 只能告诉你当前缓冲区中的字节数量,不能证明哪些字节已经被底层接受,也不能替代原始业务数据。恢复策略应以可重建的输入或事务边界为基础。
最短的记忆规则是什么?
写入错误要返回,Flush 错误要返回,Close 始终要执行且它的错误也要处理;Flush 失败后不要把对同一个 Writer 的重复 Flush 当成恢复。
Transformers generate 的停止条件与批量输出
- 上一篇
- Transformers generate 的停止条件与批量输出
- 下一篇
- CNCF Karmada 毕业对多集群编排落地的启示
-
- Golang · Go教程 | 1小时前 | Go教程 · Go bufio.Reader ErrBufferFull ReadSlice
- Go bufio.Reader ReadSlice 分片处理超长行
- 233浏览 收藏
-
- Golang · Go教程 | 1小时前 | 网络编程 · go · bufio · peek Go bufio.Reader 协议头
- Go bufio.Reader Peek 预读协议头的参数边界
- 265浏览 收藏
-
- Golang · Go教程 | 2小时前 | 网络编程 · TCP · Go教程 · Go encoding/binary io.ReadFull ErrUnexpectedEOF 定长协议帧
- Go io.ReadFull 读取定长协议帧的补齐策略
- 295浏览 收藏
-
- Golang · Go教程 | 2小时前 | 性能监控 · Go教程 · Go io.TeeReader io.Reader io.Writer 上传流量
- Go io.TeeReader 记录上传流量而不改变数据流
- 379浏览 收藏
-
- Golang · Go教程 | 3小时前 | go · Go 转义规则 方括号 filepath.Match
- Go filepath.Match 处理方括号模式的转义规则
- 222浏览 收藏
-
- Golang · Go教程 | 4小时前 | 标准库 · Go教程 · 相对路径 Go 跨平台 path/filepath filepath.Rel
- Go filepath.Rel 计算相对路径的跨平台用法
- 412浏览 收藏
-
- Golang · Go教程 | 4小时前 | Go教程 · Go 日志脱敏 URL.Redacted url.URL Userinfo
- Go url.URL Userinfo 字段的脱敏输出方式
- 246浏览 收藏
-
- Golang · Go教程 | 5小时前 | HTTP · Go教程 · Go net/url 请求目标 url.ParseRequestURI
- Go url.ParseRequestURI 处理请求目标的边界
- 174浏览 收藏
-
- Golang · Go教程 | 5小时前 | HTTP · go · Go 查询参数 url.Values
- Go url.Values 批量合并查询参数的覆盖规则
- 493浏览 收藏
-
- 前端进阶之JavaScript设计模式
- 设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
- 543次学习
-
- GO语言核心编程课程
- 本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
- 516次学习
-
- 简单聊聊mysql8与网络通信
- 如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
- 500次学习
-
- JavaScript正则表达式基础与实战
- 在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
- 487次学习
-
- 从零制作响应式网站—Grid布局
- 本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
- 485次学习
-
- PubMedQA
- 深入了解PubMedQA生物医学问答数据集,涵盖其核心功能、使用方法及在临床决策、药物研发等场景的应用,助力提升NLP模型性能。
- 256次使用
-
- H2O EvalGPT
- H2O EvalGPT是H2O.ai推出的开源LLM评估平台,提供详细的大模型性能排行榜、行业特定基准测试及A/B测试功能,助您快速选择最适合项目的高性能大语言模型。
- 301次使用
-
- LMArena
- LMArena是加州大学伯克利分校推出的AI模型匿名评测平台。通过盲测投票机制,用户可对比不同大模型回答并生成实时排行榜,助力开发者优化模型及用户选择最佳AI工具。
- 277次使用
-
- HELM
- 深入了解斯坦福推出的HELM(Holistic Evaluation of Language Models)大模型评测体系。本文解析其核心功能、安装配置步骤及应用场景,涵盖准确性、公平性、鲁棒性等多维度指标,助力开发者全面优化语言模型性能。
- 257次使用
-
- MMBench
- MMBench是由上海人工智能实验室等机构联合推出的多模态基准测试平台,提供细粒度能力评估、大规模数据集及VLMEvalKit工具。本文详细介绍其核心功能、安装使用方法及应用场景,助力开发者全面评估多模态模型性能。
- 62次使用
-
- Go语言文件读写操作案例详解
- 2022-12-29 140浏览
-
- Go代码规范错误处理示例经验总结
- 2022-12-23 278浏览
-
- Go语言文件开关及读写操作示例
- 2023-02-24 301浏览
-
- 分析Go错误处理优化go recover机制缺陷
- 2023-01-01 483浏览
-
- Go 错误处理实践总结示例
- 2023-01-07 291浏览

