Go os.OpenFile O_CREATE 没有 O_EXCL 时为什么可能覆盖竞态
来源:17golang原创
时间:2026-09-11 13:33:19 433浏览 收藏
两个 Go worker 同时生成 daily.json,日志却显示它们都“打开成功”,最后文件内容时好时坏。问题通常不在 Go 的 File 并发安全,而在于把 O_CREATE 误当成了“只允许一个创建者”。
需要竞争同一个文件名时,使用os.O_CREATE|os.O_EXCL。第一个成功打开的调用者获得创建资格;其他调用者应把os.ErrExist当作“已有生产者”的结果,而不是继续写同一路径。
O_CREATE是“没有就创建,有就打开”,单独使用不会拒绝第二个调用者。O_EXCL必须和O_CREATE一起用,才能让文件名争抢成为一次性判定。O_CREATE本身不截断文件;真正的覆盖风险来自后续写入、O_TRUNC或多个写者的时序。
O_CREATE 只是兜底创建,不是独占创建
官方 os.OpenFile 文档把 O_CREATE 定义为:目标不存在时创建;如果目标已经存在,调用仍然可以成功打开它。O_EXCL 的含义则是与 O_CREATE 联用,文件必须不存在。三者边界可以这样记:
| 标志 | 目标不存在 | 目标已存在 | 是否独占 |
|---|---|---|---|
O_CREATE | 创建并打开 | 打开已有文件 | 否 |
O_CREATE|O_EXCL | 创建并打开 | 返回 os.ErrExist | 是 |
O_CREATE|O_TRUNC | 创建并打开 | 打开后截断 | 否 |

所以,O_CREATE 单独出现时并不等于“覆盖已有文件”。它只是把两个调用者导向同一个已存在对象;如果后面使用普通 Write、显式 Seek(0, 0),或者叠加 O_TRUNC,才可能出现内容互相覆盖、交错或被截断。
没有 O_EXCL 时,竞态发生在打开之后
常见的生产代码是:
f, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE, 0o600)
if err != nil {
return err
}
defer f.Close()
// 中文注释:多个调用者都可能拿到同一路径的描述符,下面的写入没有“赢家”概念。
_, err = f.Write(payload)
return err
当路径还不存在时,调用 A 负责创建;调用 B 紧接着发现它已经存在,于是也成功返回。两个描述符的当前偏移都可能从文件开头开始,写入顺序由调度和系统时序决定。若其中一方先写入较长内容,另一方再从开头写入较短内容,还可能残留旧尾部;若代码用了 O_TRUNC,后打开者会直接把前一个写者的内容清空。
这也是为什么“给每个 goroutine 加互斥锁”不一定够:锁只覆盖同一个进程,无法约束另一个进程、另一个容器或重启后恢复的任务。真正要保护的是文件名的创建资格。
用 O_CREATE|O_EXCL 让一个调用者成为赢家
把判断交给一次打开操作,不要先 Stat 再决定是否创建。后者把检查和创建拆成两次系统调用,中间始终存在窗口。
package main
import (
"errors"
"fmt"
"os"
)
func createOnce(path string, payload []byte) error {
// 中文注释:O_EXCL 与 O_CREATE 联用,要求路径在打开瞬间仍不存在。
f, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0o600)
if err != nil {
// 中文注释:ErrExist 表示别的 worker 已占用文件名,调用方不应继续写入。
if errors.Is(err, os.ErrExist) {
return fmt.Errorf("文件已由其他任务创建: %w", err)
}
return fmt.Errorf("独占创建文件失败: %w", err)
}
// 中文注释:写入失败时关闭并删除半成品,避免下一次任务误把它当成完整文件。
if _, err := f.Write(payload); err != nil {
_ = f.Close()
_ = os.Remove(path)
return fmt.Errorf("写入新文件失败: %w", err)
}
// 中文注释:关闭失败也要报告,调用方可决定是否重试或清理文件。
if err := f.Close(); err != nil {
_ = os.Remove(path)
return fmt.Errorf("关闭新文件失败: %w", err)
}
return nil
}
这个函数的关键不是“写得更快”,而是把“谁有资格创建”与“谁负责写入”绑定到同一个成功结果。失败者收到 os.ErrExist 后,可以转去读取已完成文件、等待 ready 标记,或记录“本次任务已被其他 worker 处理”。

修复后还要处理半成品和读取时机
O_EXCL 解决的是“名称只能被一个创建者拿到”,不是“读者永远看不到半成品”。赢家打开成功后到写完关闭前,其他读者仍可能看到一个正在增长的文件。因此要先明确业务约束:
- 读者允许等待:写入成功后再创建
daily.json.ready,读者只处理存在 ready 标记的文件。 - 读者必须只见完整内容:先在同目录用唯一临时名独占创建,写完并关闭,再采用符合目标系统语义的原子发布方案;不要跨文件系统移动。
- 文件是追加日志:使用
O_APPEND,并为每条记录设计可恢复的边界;它不能替代“只创建一次”的O_EXCL。
另外,os.OpenFile 返回成功后要确保每条错误路径都关闭描述符。写入半途失败时删除文件是否安全,取决于是否可能有读者已经拿到它;有读者时更适合把文件标记为失败并异步清理。
并发创建文件的检查清单
- 目标是“没有就创建”还是“只能有一个创建者”?后者必须包含
O_EXCL。 - 是否先
Stat、再OpenFile?如果是,应改为一次O_CREATE|O_EXCL。 - 是否误加了
O_TRUNC?它会把已有普通文件截断,和独占创建是两个问题。 - 失败者收到
os.ErrExist后是否停止写入?继续写就是竞态重新出现。 - 读者如何判断文件写完?必要时增加 ready 标记、临时文件发布或任务状态。
常见问题
O_CREATE 不加 O_TRUNC 会自动清空旧文件吗?
不会。O_CREATE 只控制不存在时创建;已有文件通常会被打开,是否清空取决于 O_TRUNC 或后续写入方式。
同一进程的多个 goroutine 只用 O_EXCL 就够了吗?
它足以保护文件名的创建资格,但不能自动保证读者看不到半成品,也不能替你处理写入失败后的清理。
为什么不先 os.Stat 判断文件是否存在?
因为 Stat 和后续创建不是一个不可分割的动作,另一个调用者可以在两者之间抢先创建。把判断放进 O_CREATE|O_EXCL 的打开操作中,才不会留下这个窗口。
参考资料:https://pkg.go.dev/os#OpenFile、https://go.dev/src/os/file.go。它们分别说明 OpenFile 的创建语义以及 O_CREATE、O_EXCL、O_TRUNC 等标志的定义。
-
860 收藏
-
843 收藏
-
826 收藏
-
809 收藏
-
792 收藏
-
493 收藏
-
403 收藏
-
251 收藏
-
249 收藏
-
431 收藏
-
Golang · Go教程 | 1小时前 | 字符串处理 · Go教程 · 兼容旧版本 · CutPrefix · 协议前缀 · Go 字符串前缀 HasPrefix strings.CutPrefix366 收藏
-
Golang · Go教程 | 1小时前 | 标准库 · go字符串 · Go教程 · 代码实践 · API对比 · Go 字符串前缀 HasPrefix strings.CutPrefix strings.TrimPrefix447 收藏
-
494 收藏
-
398 收藏
-
292 收藏
-
Golang · Go教程 | 2小时前 | 标准库 · 正则表达式 · Go教程 · 数据导入 · CSV解析 · csv Go encoding/csv csv.Reader regexp.Split112 收藏
-
451 收藏
-
- 前端进阶之JavaScript设计模式
- 设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
- 立即学习 543次学习
-
- GO语言核心编程课程
- 本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
- 立即学习 516次学习
-
- 简单聊聊mysql8与网络通信
- 如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
- 立即学习 500次学习
-
- JavaScript正则表达式基础与实战
- 在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
- 立即学习 487次学习
-
- 从零制作响应式网站—Grid布局
- 本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
- 立即学习 485次学习