会丢数据。因main退出时Go不等待后台goroutine完成,直接终止进程;必须用sync.WaitGroup同步等待或channel+守护goroutine消费日志,且需显式file.Sync()确保落盘。

goroutine 启动日志写入会不会丢数据
直接用 go log.Printf(...) 或 go writer.Write(...) 启动 goroutine 写日志,极大概率丢数据。因为主 goroutine 退出时,其他 goroutine 可能还没执行完,运行时不会等待——Go 不保证后台 goroutine 完成。
必须显式同步:要么用 sync.WaitGroup 阻塞主流程直到写入完成,要么把写入转为异步队列 + 守护 goroutine 持续消费(更常用)。
- 短命命令行工具适合
WaitGroup+ 批量写入后统一等待 - 长期运行服务(如 HTTP server)必须用带缓冲 channel 的生产者-消费者模型
- 别用无缓冲 channel 做日志通道,容易卡住发送方
用 channel + goroutine 构建安全日志管道
核心是分离「记录动作」和「实际 I/O」:业务代码只往 channel 发日志结构体,专用 goroutine 从 channel 读取并落盘。这样既解耦又可控。
示例关键片段:
立即学习“go语言免费学习笔记(深入)”;
type LogEntry struct {
Level string
Msg string
Time time.Time
}
logCh := make(chan LogEntry, 1000) // 缓冲区防阻塞
<p>go func() {
file, _ := os.OpenFile("app.log", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644)
defer file.Close()
for entry := range logCh {
fmt.Fprintf(file, "[%s] %s: %s\n", entry.Time.Format("2006-01-02 15:04:05"), entry.Level, entry.Msg)
file.Sync() // 关键:确保刷到磁盘,否则崩溃可能丢最后几条
}
}()</p><p>// 业务中调用
logCh <- LogEntry{Level: "INFO", Msg: "user logged in", Time: time.Now()}
-
file.Sync()在高可靠性场景不能省,但会拖慢吞吐;可改用定期 flush +fsync折中 - 缓冲大小设为 1000 是经验值,若日志峰值突增,需结合
select+default做丢弃或降级 - channel 关闭前务必先 close(
logCh),否则消费者 goroutine 永远阻塞在range
为什么不用 logrus/zap 的内置 goroutine 模式
第三方库如 logrus 默认同步写,zap 的 core 也默认同步。它们所谓「异步」其实是把日志编码 + I/O 封装进一个 goroutine,但底层仍是单个写入 goroutine —— 和手写 channel 模型本质一样,只是封装了细节。
真正差异在于错误处理和生命周期管理:
-
zap.NewProductionConfig().AddCaller()开启的异步模式,崩溃时未写入日志会静默丢失,无重试 - 手写管道可加
time.AfterFunc定期检查 channel 长度,超阈值触发告警 - 若需多后端(文件 + 网络 + 标准输出),手写 channel 更易扩展:一个源,多个消费者 goroutine
关闭程序时如何安全退出日志 goroutine
最常被忽略的点:进程收到 SIGINT 或 SIGTERM 时,不能直接 os.Exit(0),否则日志 goroutine 中 pending 的消息全丢。
正确做法是发信号通知日志 goroutine 停止,并等待它清空队列:
- 用
context.WithCancel控制 consumer 生命周期 - 关闭前调用
cancel(),consumer 收到 context.Done() 后跳出for循环 - 再用
close(logCh)让剩余消息被消费完,然后才退出 - 别依赖
defer关文件——defer 在 main return 后执行,但 goroutine 可能已终止
实际关机逻辑比启动复杂得多,尤其当 channel 缓冲区大、磁盘慢、或日志量突发时,这个收尾步骤决定日志完整性底线。


















