直接并发调用*os.file.write会因共享文件偏移量和缓冲区导致内容错乱、截断或panic;sync.mutex加锁适用于中小流量场景,而高吞吐应采用channel+单goroutine消费模式。

为什么直接用 os.File.Write 并发写会出问题
多个 goroutine 同时调用同一个 *os.File.Write,不加同步会导致日志内容错乱、截断甚至 panic(比如 write /path/log: bad file descriptor)。底层文件描述符是共享的,Write 不是原子操作,且不同 goroutine 的写入可能互相覆盖缓冲区或偏移位置。
用 sync.Mutex 保护单个文件句柄最简单可靠
适用于中小流量场景(每秒几百条日志),实现轻量、无第三方依赖:
type SafeLogger struct {
mu sync.Mutex
file *os.File
}
func (l *SafeLogger) Write(p []byte) (n int, err error) {
l.mu.Lock()
defer l.mu.Unlock()
return l.file.Write(p)
}
- 必须在
Write入口加锁,不能只锁WriteString或格式化逻辑 - 避免在锁内做耗时操作(如调用
time.Now()是安全的;但不要在锁里发起 HTTP 请求) - 注意:如果日志量极大(>1k QPS),锁会成为瓶颈,此时应考虑分片或异步队列
用 chan + 单 goroutine 消费实现真正解耦
适合高吞吐或需控制写入顺序/频率的场景,把并发写转为串行落盘:
初始化并管理阿里云SDK Go 客户端,涵盖 sync.Once 单例、goroutine 安全、endpoint 与 region 配置、VPC 端点及调试。
type LogEntry struct {
Level string
Msg string
Time time.Time
}
func NewAsyncLogger(filename string) (*AsyncLogger, error) {
f, err := os.OpenFile(filename, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644)
if err != nil {
return nil, err
}
ch := make(chan LogEntry, 1000) // 缓冲区防阻塞生产者
al := &AsyncLogger{ch: ch, file: f}
go al.writer()
return al, nil
}
func (al *AsyncLogger) writer() {
for entry := range al.ch {
_, _ = fmt.Fprintf(al.file, "[%s] %s %s\n", entry.Time.Format("2006-01-02 15:04:05"), entry.Level, entry.Msg)
_ = al.file.Sync() // 确保刷盘,按需启用
}
}
- 缓冲区大小要权衡:太小导致生产者阻塞,太大增加内存占用和宕机丢日志风险
-
file.Sync()保证数据落盘,但显著降低性能;若允许少量丢失(如调试日志),可去掉 - 务必在程序退出前关闭
chan并等待 goroutine 结束,否则可能漏写最后几条
别忽略文件轮转和权限问题
生产环境几乎一定会遇到日志滚动、磁盘满、权限不足等情况,而这些在并发下更易暴露:
- 轮转时若直接
os.Rename原文件,正在写的*os.File句柄仍指向旧 inode,新日志继续写入已重命名的文件 —— 必须用os.Truncate或重新OpenFile并替换句柄 - 多 goroutine 下检查磁盘空间再写,可能刚检查完就写失败;建议写失败后捕获
syscall.ENOSPC并降级处理(如打印到 stderr) - Linux 下若以非 root 启动,确保日志目录对运行用户有
w权限,否则并发写可能部分成功部分失败,错误不易复现
真正难的不是并发写本身,而是轮转、错误恢复、资源清理这些边界情况 —— 它们往往在压测或上线后才浮现。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!










