Go写入前需先用os.Stat判断文件是否存在以决定是否备份,备份文件名须含高精度时间戳,写入应通过临时文件+原子重命名实现,返回writeErr和backupErr两个错误以便调用方区分处理。

写入前判断目标文件是否存在
Go 里没有内置的「覆盖即备份」机制,必须手动检查 os.Stat 或 os.ReadFile 是否返回 os.IsNotExist 错误。如果目标文件已存在,就该触发备份逻辑;否则直接写入即可。
注意:不能只靠 os.OpenFile(..., os.O_CREATE|os.O_WRONLY) 的返回错误来判断——它在文件存在时不会报错,会直接截断写入,旧内容彻底丢失。
- 用
_, err := os.Stat("config.yaml")</li> <li>若 <code>err == nil,说明文件存在,进入备份流程 - 若
os.IsNotExist(err),可安全写入,跳过备份
备份文件名生成要带时间戳而非简单加 .bak
只重命名为 config.yaml.bak 会导致多次写入后反复覆盖备份,上次改的配置可能永远丢掉。推荐用 time.Now().Format("20060102150405") 生成唯一后缀。
示例:config.yaml.20240520142301 比 config.yaml.bak 可追溯得多,也方便脚本清理(比如只保留最近 3 个)。
立即学习“go语言免费学习笔记(深入)”;
- 避免使用
time.Now().Unix()——秒级精度在快速连续写入时可能重复 - 不要用
filepath.Join(dir, base+".bak")这种静态拼接 - 路径中含中文或特殊字符时,
os.Rename在 Windows 下可能失败,建议统一用os.Copy+os.Remove
备份 + 写入需原子性,防止中间出错导致双损
最危险的情况是:备份成功了,但新文件写入失败(磁盘满、权限不足等),结果旧文件被删、新文件没写成,配置全丢。
正确顺序是:先写新内容到临时文件(如 config.yaml.tmp),再确认写入完整,最后原子替换 —— 即 os.Rename 临时文件为目标文件。备份动作放在 rename 之前,且独立于写入过程。
- 临时文件务必和目标文件同目录(跨分区
os.Rename会失败) - 写完临时文件后,用
os.Chmod(tmpPath, fi.Mode())复制原文件权限 - 备份操作建议异步记录日志,但不要阻塞主流程;若备份失败,只要 rename 成功,系统仍可用
实际封装函数要注意 error 返回粒度
别把「备份失败」和「写入失败」混成一个 error 返回。调用方需要区分:是配置根本没更新成功(写入失败),还是只是历史版本没留好(备份失败)。
常见做法是返回两个 error:writeErr, backupErr。这样上层可选择忽略 backupErr(比如仅用于审计,不阻断服务),但绝不能忽略 writeErr。
- 不要用 panic 处理
os.Rename失败——这是预期中的运维异常 -
ioutil.WriteFile已弃用,统一用os.WriteFile(Go 1.16+) - 如果目标文件是 JSON/YAML 配置,写入前建议用
json.MarshalIndent或对应库做格式化,避免备份文件可读性差


















