应使用 log.setoutput 将日志输出切换至文件,通过 os.openfile 一次打开并复用文件句柄,避免覆盖、乱序、静默失败及句柄泄漏;需配合 os.mkdirall 确保目录存在,权限控制须额外调用 os.chmod 或依赖运维工具。

用 log.SetOutput 切换日志输出到文件而不是控制台
Go 标准库的 log 包默认往 os.Stderr 写,想存文件就得主动换掉输出目标。关键不是“怎么写文件”,而是“怎么让 log 用你给的文件句柄”。
常见错误是直接用 os.WriteFile 拼字符串再写——这绕过了 log 的格式、锁、前缀等机制,多 goroutine 下容易乱序或丢日志。
- 先用
os.OpenFile以os.O_CREATE | os.O_WRONLY | os.O_APPEND模式打开文件,避免覆盖历史日志 - 传给
log.SetOutput前,确认文件对象没被提前Close;否则后续日志会静默失败(Write返回io.ErrClosedPipe但log不报错) - 如果程序长期运行,注意文件句柄泄漏:不要每次写日志都
OpenFile,而应在启动时打开一次并复用
示例:
f, err := os.OpenFile("app.log", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644)
if err != nil {
log.Fatal(err)
}
log.SetOutput(f)
log.Println("service started") // 这行会进文件
为什么不用 log.Printf 而要用 log.New 自定义 logger
全局 log 包(log.Printf 等)是单例,多个模块共用同一输出、前缀、flag,一改全改。实际项目里,HTTP 请求日志、数据库慢查询、定时任务日志往往需要不同格式或不同文件。
用 log.New 可隔离行为:
立即学习“go语言免费学习笔记(深入)”;
- 每个 logger 可独立设置前缀(如
"http:"、"db:")、flag(是否带时间、文件名、行号) - 输出目标可以是不同文件、
io.MultiWriter(同时写文件+网络)、甚至内存 buffer(用于测试) - 性能上无额外开销:底层仍是
io.Writer.Write,和全局 logger 一样轻量
示例:
accessLog := log.New(
accessFile,
"http: ",
log.LstdFlags|log.Lshortfile,
)
accessLog.Printf("200 GET /health %s", time.Since(start))
os.OpenFile 的 perm 参数在 Windows 和 Linux 上表现不一致
Linux 下 os.OpenFile 的 perm(如 0644)只影响新建文件的权限;已存在文件的权限不会被修改。Windows 完全忽略该参数——无论设多少,新建文件都是可读写,且无法通过此参数设只读。
这意味着:
- 别指望靠
perm实现“日志文件仅限 owner 可读”,得靠后续调用os.Chmod(Linux)或系统策略(Windows) - 在容器环境(如 Docker)中,挂载卷的 umask 可能覆盖
perm,导致文件实际权限比预期更宽松 - 如果日志路径父目录不存在,
os.OpenFile不会自动创建,需先用os.MkdirAll,否则返回no such file or directory
日志滚动不是标准库的事,别硬套 log 包解决
Go 标准 log 包不支持按大小/时间自动切分文件。有人试图在每次写入前检查文件大小再 Close+OpenFile 新文件——这有竞态:两个 goroutine 同时检查、同时决定轮转,结果一个覆盖另一个。
正确做法是交给专门的日志库或外部工具:
- 简单场景:用
lumberjack(第三方包),它实现了线程安全的轮转,可配MaxSize、MaxAge、MaxBackups - 生产环境:更推荐把日志输出到 stdout,由 systemd、docker、k8s 或 logrotate 统一收集和轮转——解耦应用逻辑与运维策略
- 若坚持手写轮转,必须加互斥锁(
sync.Mutex)保护文件切换过程,且轮转后要确保所有 logger 都指向新文件(比如用原子指针更新*os.File)
真正难的不是“怎么切文件”,而是“怎么保证切的时候不丢日志、不阻塞主线程、不污染其他 logger 实例”——这些细节很容易被忽略,直到压测时出现日志断层或 panic。










