当前位置:首页 > 文章列表 > Golang > Go教程 > Go os.OpenFile O_CREATE 没有 O_EXCL 时为什么可能覆盖竞态

Go os.OpenFile O_CREATE 没有 O_EXCL 时为什么可能覆盖竞态

来源:17golang原创 2026-09-11 13:33:19 0浏览 收藏

两个 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创建并打开打开后截断
Go os.OpenFile O_CREATE 连接目标路径、已有文件、文件描述符和写入偏移的静态语义框图
图1:看清 O_CREATE 的“不存在才创建、存在仍可打开”关系,避免把它当作独占锁。

所以,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 处理”。

Go O_CREATE O_EXCL 将两个 worker 连接到同一路径并分成赢家文件描述符和 ErrExist 结果的静态关系框图
图2:O_CREATE|O_EXCL 的结果分成“唯一文件描述符”和“文件已存在错误”,后者不再进入写入路径。

修复后还要处理半成品和读取时机

O_EXCL 解决的是“名称只能被一个创建者拿到”,不是“读者永远看不到半成品”。赢家打开成功后到写完关闭前,其他读者仍可能看到一个正在增长的文件。因此要先明确业务约束:

  • 读者允许等待:写入成功后再创建 daily.json.ready,读者只处理存在 ready 标记的文件。
  • 读者必须只见完整内容:先在同目录用唯一临时名独占创建,写完并关闭,再采用符合目标系统语义的原子发布方案;不要跨文件系统移动。
  • 文件是追加日志:使用 O_APPEND,并为每条记录设计可恢复的边界;它不能替代“只创建一次”的 O_EXCL

另外,os.OpenFile 返回成功后要确保每条错误路径都关闭描述符。写入半途失败时删除文件是否安全,取决于是否可能有读者已经拿到它;有读者时更适合把文件标记为失败并异步清理。

并发创建文件的检查清单

  1. 目标是“没有就创建”还是“只能有一个创建者”?后者必须包含 O_EXCL
  2. 是否先 Stat、再 OpenFile?如果是,应改为一次 O_CREATE|O_EXCL
  3. 是否误加了 O_TRUNC?它会把已有普通文件截断,和独占创建是两个问题。
  4. 失败者收到 os.ErrExist 后是否停止写入?继续写就是竞态重新出现。
  5. 读者如何判断文件写完?必要时增加 ready 标记、临时文件发布或任务状态。

常见问题

O_CREATE 不加 O_TRUNC 会自动清空旧文件吗?

不会。O_CREATE 只控制不存在时创建;已有文件通常会被打开,是否清空取决于 O_TRUNC 或后续写入方式。

同一进程的多个 goroutine 只用 O_EXCL 就够了吗?

它足以保护文件名的创建资格,但不能自动保证读者看不到半成品,也不能替你处理写入失败后的清理。

为什么不先 os.Stat 判断文件是否存在?

因为 Stat 和后续创建不是一个不可分割的动作,另一个调用者可以在两者之间抢先创建。把判断放进 O_CREATE|O_EXCL 的打开操作中,才不会留下这个窗口。

参考资料:https://pkg.go.dev/os#OpenFilehttps://go.dev/src/os/file.go。它们分别说明 OpenFile 的创建语义以及 O_CREATE、O_EXCL、O_TRUNC 等标志的定义。

版本声明
本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
Go Go modules vendor 之后为什么仍可能访问网络Go Go modules vendor 之后为什么仍可能访问网络
上一篇
Go Go modules vendor 之后为什么仍可能访问网络
Linux mount --bind 解除挂载时为什么要先离开工作目录
下一篇
Linux mount --bind 解除挂载时为什么要先离开工作目录
查看更多
最新文章
查看更多
课程推荐
  • 前端进阶之JavaScript设计模式
    前端进阶之JavaScript设计模式
    设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
    543次学习
  • GO语言核心编程课程
    GO语言核心编程课程
    本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
    516次学习
  • 简单聊聊mysql8与网络通信
    简单聊聊mysql8与网络通信
    如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
    500次学习
  • JavaScript正则表达式基础与实战
    JavaScript正则表达式基础与实战
    在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
    487次学习
  • 从零制作响应式网站—Grid布局
    从零制作响应式网站—Grid布局
    本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
    485次学习
查看更多
AI推荐
  • H2O EvalGPT:开源LLM大模型评估与排行榜工具
    H2O EvalGPT
    H2O EvalGPT是H2O.ai推出的开源LLM评估平台,提供详细的大模型性能排行榜、行业特定基准测试及A/B测试功能,助您快速选择最适合项目的高性能大语言模型。
    81次使用
  • SuperCLUE中文大模型评测基准:功能、能力维度与应用指南
    SuperCLUE
    SuperCLUE是权威的中文大语言模型综合评测基准,涵盖语言理解、知识应用、AI Agent智能体及安全性等12项核心能力。通过多轮对话与客观测试,定期发布榜单与技术报告,为模型研发、优化及行业选型提供科学依据。
    239次使用
  • C-Eval中文评测基准:大语言模型多学科能力评估指南
    C-Eval
    深入了解C-Eval中文评估套件,涵盖52个学科与4级难度。本文详解其功能特点、Zero-shot/Few-shot使用方法及代码示例,助您全面评测LLM中文理解与泛化能力。
    166次使用
  • AI Prompt Library:免费AI提示词库,助力ChatGPT高效创作与营销
    AI Prompt Library
    探索AI Prompt Library免费资源库,涵盖营销、写作及多场景AI提示词。兼容ChatGPT、Claude等工具,一键复制优化输出,提升工作效率。
    100次使用
  • Generrated:DALL·E 2/3 AI绘画提示词灵感库与图像对比平台
    Generrated
    Generrated汇集9300+张DALL·E生成图像及对应提示词,支持查看完整图集、对比DALL·E 2与3版本差异,是AI绘图新手学习Prompt设计与获取创作灵感的实用工具。
    77次使用
微信登录更方便
  • 密码登录
  • 注册账号
登录即同意 用户协议隐私政策
返回登录
  • 重置密码