os.openfile追加必须同时指定os.o_append、os.o_wronly、os.o_create:缺o_append会覆盖写,缺o_wronly报invalid argument,缺o_create则文件不存在时失败。

os.OpenFile 的 flag 组合必须同时含 os.O_APPEND、os.O_WRONLY 和 os.O_CREATE
只写不追加、或追加但没权限创建文件,都会失败。漏掉 os.O_WRONLY 会报 invalid argument(Linux 下 open 系统调用拒绝仅带 O_APPEND 的只读打开);混入 os.O_TRUNC 则行为未定义,实际多数系统直接清空文件——哪怕你写了 O_APPEND | O_TRUNC,也别这么干。
权限设为 0644 即可,Windows 忽略该参数,但 Unix-like 系统下缺它可能导致首次创建后无法再写。
-
os.O_APPEND是核心:它让每次Write都从当前文件末尾开始,无需手动Seek,且由内核保证 offset 原子性 -
os.O_WRONLY不可省:没有写权限,O_APPEND就没意义 -
os.O_CREATE要带上:否则文件不存在时直接返回no such file or directory
并发写同一文件不能靠 *os.File 自身“线程安全”幻想
*os.File.Write 底层调的是系统 write(2),单次调用在多数系统上是原子的,但 fmt.Fprintf 或 file.WriteString 是复合操作:格式化 → 拷贝到 buffer → syscall。多个 goroutine 同时走这套流程,buffer 竞争、offset 同步延迟、调度间隙都可能造成两行内容挤在同一行,甚至截断(如 "user:123" 和 "login\n" 写成 "user:logi123n\n")。
加 sync.Mutex 能解决,但锁粒度粗、易阻塞、难扩展;更关键的是,它没解决“谁来负责关闭文件”“panic 时 buffer 是否丢数据”这些现实问题。
- 不要共享一个
*os.File让多个 goroutine 直接WriteString - 不要用
os.WriteFile并发写同一路径:它内部Create+Write+Close,必然覆盖前一次 - 避免
os.Seek(f, 0, io.SeekEnd)模拟追加:多进程/多 goroutine 下不可靠,且跨平台行为不一致
用 channel 实现写入服务是最稳妥的 CSP 方式
把文件句柄锁死在一个 goroutine 里,其他协程只发消息,由这个“写入服务”串行处理。它天然规避竞态,逻辑边界清晰,后续加缓冲、重试、格式化都容易。
示例核心结构:
type LogEntry struct {
Msg string
}
logCh := make(chan LogEntry, 1000)
go func() {
f, _ := os.OpenFile("app.log", os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0644)
defer f.Close()
w := bufio.NewWriter(f)
defer w.Flush()
for entry := range logCh {
w.WriteString(entry.Msg + "\r\n") // Windows 友好,人工查看不乱行
w.Flush() // 显式刷,防 panic 丢数据
}
}()
// 其他 goroutine 发送:
logCh
- channel 缓冲区大小按吞吐预估,太小会阻塞发送方,太大占内存
-
bufio.NewWriter必须配Flush(),否则最后一段永远卡在 buffer 里 - 换行统一用
"\r\n",尤其目标是 Windows 记事本查看时
真正麻烦的从来不是 O_APPEND,而是失败后怎么办
O_APPEND 解决了“写到哪”,但没回答“写失败了是否重试”“磁盘满了怎么告警”“日志丢了能否接受”。比如 f.Sync() 能确保落盘,但每条都 Sync 会让吞吐暴跌;而完全不 Sync,断电就丢最后几 KB。
错误判断别信字符串,用 errors.Is(err, syscall.ENOSPC) 判磁盘满,errors.Is(err, syscall.EACCES) 判权限不足——这些信号比 err != nil 有用得多。
还有个隐形坑:defer f.Close() 放在 goroutine 里,如果 channel 关闭后没显式退出,f 可能长期不关,句柄泄漏。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











