写入失败时必须无条件检查err != nil并保存原始error,而非仅用os.IsNotExist判断;应使用结构体聚合失败信息,区分可重试与需告警错误,并发写入需同步保护。

写入失败时必须捕获 error,不能只靠 os.IsNotExist
批量写入文件时,失败原因远不止“目录不存在”。常见却被忽略的包括:磁盘满(ENOSPC)、权限不足(EACCES)、路径过长(ENAMETOOLONG)、父目录不可写(EPERM on macOS)、甚至 NFS 挂载点临时断连。只检查 os.IsNotExist(err) 会漏掉绝大多数真实错误。
正确做法是:每次 os.WriteFile 或 io.WriteString 后,**无条件检查 err != nil**,并把原始 err 保存下来——它自带上下文(如具体路径、系统错误码),比手动拼字符串更可靠。
- 别用
log.Printf替代错误收集;日志输出不等于可回溯的失败记录 - 避免在循环里直接 panic 或 return;要继续处理后续文件
- 如果用
os.Create+Write,记得显式Close(),否则可能因句柄泄漏导致后续写入失败但报错不明确
用结构体聚合失败信息,别用 map[string]string 硬编码字段
批量写入后需要汇总失败项,用于重试或告警。用 map 存路径+错误消息看似简单,但丢失了错误类型、时间戳、重试建议等关键维度。推荐定义一个轻量结构体:
type WriteFailure struct {
Path string
Err error
Attempt int
Timestamp time.Time
}
这样后续可轻松过滤:failures[i].Err == syscall.ENOSPC,或按时间排序排查突发性磁盘问题。若用 map[string]string,所有错误都被转成字符串,无法做类型判断。
立即学习“go语言免费学习笔记(深入)”;
- 字段
Err类型必须是error,不是string;Go 的%w包装链能保留原始错误上下文 - 如果需 JSON 序列化(如上报到监控系统),给结构体加
json:tag,但注意error类型默认不序列化,应额外提供ErrMsg() string方法 - 不要把失败路径拼进错误消息里(如
fmt.Errorf("write %s failed: %w", path, err));路径已是结构体字段,重复冗余
并发写入时,append 到切片需加锁或用 sync.Slice(Go 1.21+)
用 golang.org/x/sync/errgroup 并发写入时,多个 goroutine 同时 append 到同一个 []WriteFailure 会导致数据竞争。Go 1.21 前必须用 sync.Mutex 保护切片操作;Go 1.21 起可用 sync.Slice 避免手写锁:
var failures sync.Slice[WriteFailure]
eg.Go(func() error {
if err := os.WriteFile(path, data, 0644); err != nil {
failures.Append(WriteFailure{Path: path, Err: err, Timestamp: time.Now()})
return nil // 不返回 err,避免 errgroup 提前退出
}
return nil
})
注意:errgroup 的设计意图是“任一子任务出错则整体失败”,但这里我们**主动吞掉写入错误**,仅记录,所以每个 goroutine 都应返回 nil。
- 别用
chan WriteFailure收集结果再range转切片;小批量时性能无差别,但 channel 关闭逻辑易出错 - 如果并发数极高(>1000),考虑用预分配切片 + 原子计数器分段写入,避免锁争用
- 测试时用
runtime.GOMAXPROCS(1)可复现竞态,但不能替代go run -race
记录失败后,别忘了区分“可重试”和“该报警”的错误类型
不是所有写入失败都该立刻重试。比如 syscall.ENOSPC(磁盘满)或 syscall.EIO(硬件 I/O 错误)大概率不会因重试恢复,应直接记录并触发监控告警;而 syscall.EAGAIN 或网络文件系统临时超时,则适合带退避策略重试。
实际判断逻辑建议封装为函数:
func shouldRetry(err error) bool {
var e syscall.Errno
if errors.As(err, &e) {
switch e {
case syscall.EAGAIN, syscall.EINTR, syscall.ETIMEDOUT:
return true
case syscall.ENOSPC, syscall.EIO, syscall.ENODEV:
return false
}
}
return false // 其他错误默认不重试,交由人工介入
}
- 用
errors.As解包,而非errors.Is;因为底层系统错误常被fmt.Errorf("%w")包装多层 - 别把重试逻辑和记录逻辑耦合在同一个循环里;先统一记录所有失败,再单独遍历决定哪些重试
- 重试次数建议硬编码上限(如 3 次),避免无限循环卡住批量流程
最易被忽略的是:没检查父目录是否存在且可写。即使目标文件路径合法,os.WriteFile("a/b/c.txt", ...) 仍可能因 a/b 不存在或不可写而失败——这个错误的 err 里就包含真实路径信息,必须原样保留。


















