正确做法是用 io.multiwriter 将 os.stdout 和日志文件组合成一个 io.writer,再传给 log.setoutput,使单次 log.println 同时输出到终端和文件;需用 os.o_append 模式打开文件、权限设为 0644、defer 关闭文件,并配合 tail -f 实时查看,同时注意日志换行与轮转兼容性。

log.SetOutput 配合 io.MultiWriter 实现双目标输出
要让日志既实时显示在终端又落盘到文件,核心不是写两个 logger,而是把同一条日志“一式两份”发出去。Go 标准 log 包本身不支持多输出,但 io.MultiWriter 能把多个 io.Writer 合成一个,log.SetOutput 接收它就自然生效。
常见错误是手动用 goroutine 分别写 stdout 和 file —— 这会破坏日志顺序、引入竞态,且无法保证原子性。正确做法只调一次 log.Println,由 MultiWriter 内部按顺序写入每个目标。
-
os.OpenFile必须用os.O_APPEND模式打开日志文件,否则每次写都会覆盖开头 - 权限设为
0644(而非0666),避免组/其他用户意外修改日志 - 务必
defer logFile.Close(),长期运行服务中漏掉这句会导致文件句柄耗尽 - 不要用
os.O_RDWR:日志只需写入,读权限纯属冗余,还可能被误删或篡改
tail -f 是最轻量的实时查看方案
开发阶段直接在终端跑 tail -f log.txt 就能实时看到新日志,无需改代码、不依赖额外库、零学习成本。这是 Linux/macOS 上事实标准,比任何 Go 内部轮询都可靠。
注意:Windows 用户需用 Get-Content log.txt -Wait 或安装 WSL 后用 tail;若日志文件被 logrotate 切走,tail -f 会停在旧文件末尾,此时应改用 tail -F(自动跟随重命名)。
-
tail -f本质是 inotify 监听文件 inode 变化,内核级事件驱动,无 polling 开销 - 不要在 Go 程序里自己实现“每秒读一次文件末尾”——延迟高、易丢行、CPU 白耗
- 若需 Web 页面查看日志,用
tail -f+ nginx 的proxy_buffering off即可流式推送,比 WebSocket 更简单稳定
日志行格式必须兼容 tail 解析
tail -f 按行工作,所以每条日志必须以 \n 结尾,且不能跨行拼接。Go 的 log.Println 自动加换行,但 log.Print 不加,容易导致多条日志挤在同一行,tail 就无法正确分割。
Go 配置库,使用 spf13/viper — 分层优先级(flag > env >file > KV > default),提供 BindPFlag/BindPFlags、SetEnvPrefix + SetEnvKeyReplace 等功能。
结构化日志(如 JSON)也得保证每条记录独占一行,否则 tail 流式读取时会截断不完整 JSON,后续解析失败。
- 禁用
log.Print,统一用log.Println或log.Printf("%s\n", msg) - 若用
slog,确保slog.NewJSONHandler的opts.ReplaceAttr不注入换行符 - 避免在日志消息里手动拼接
\n—— 比如log.Printf("err: %v\n%s", err, debugInfo),debugInfo 若含换行就会污染整行结构
生产环境需处理日志轮转
io.MultiWriter 不知道日志文件何时被切走,它始终往原文件句柄写。如果外部工具(如 logrotate)把 log.txt 重命名为 log.txt.1 并新建空 log.txt,你的程序还在往旧文件写,新日志就看不见了。
最稳妥解法是用 lumberjack 库替代裸 *os.File,它内部监听 os.Rename 事件并自动 reopen 新文件。但要注意:lumberjack.Logger 本身是 io.WriteCloser,可直接塞进 io.MultiWriter(os.Stdout, lumberjackLogger)。
- logrotate 配置里必须加
copytruncate(先拷贝再清空原文件),否则lumberjack会因文件被删而报错 - 不要自己监听
SIGHUP去 reopen 文件——信号处理复杂,且无法覆盖所有轮转场景 - 若坚持不用第三方库,至少让运维脚本在 rotate 后向进程发
SIGUSR1,并在 Go 中注册 handler 手动 reopen,但可靠性远低于lumberjack
真正难的不是把日志写到哪,而是确保日志在任意生命周期下(启动、轮转、崩溃重启)都能被 tail -f 稳定捕获。MultiWriter 解决输出路径,tail 解决消费路径,轮转机制解决持久性——三者缺一不可,且必须对齐行为边界。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!










