用 log.setoutput 重定向日志到文件需用 os.openfile 指定 os.o_append|os.o_create|os.o_wronly 打开文件,全局保存 *os.file 并确保程序退出前关闭;并发写日志时标准 log 包安全,但直接 write 文件需加锁;lumberjack 的 maxsize 单位为字节。

用 log.SetOutput 重定向日志到文件最直接
Go 标准库的 log 包默认输出到 os.Stderr,想写入文件只需替换输出目标。关键不是“怎么初始化”,而是“别忘了关闭文件句柄”——否则程序退出前日志可能丢失,或文件一直被占用。
常见错误现象:log.Println("hello") 执行了但文件里没内容;重启程序后旧日志被清空;多进程写同一个文件导致内容错乱。
- 用
os.OpenFile时必须传os.O_APPEND | os.O_CREATE | os.O_WRONLY,缺一不可——少O_APPEND会覆盖,少O_CREATE在文件不存在时报错 - 不要在每次写日志前都
os.OpenFile再log.SetOutput,开销大且易漏Close - 如果程序长期运行(如服务),把
*os.File存为全局变量或传入结构体,只打开一次
file, err := os.OpenFile("app.log", os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
if err != nil {
log.Fatal(err)
}
defer file.Close() // 注意:这里 defer 不适用于长生命周期程序
log.SetOutput(file)
并发写日志必须加锁,log.Logger 本身不保证线程安全
Go 的 log.Logger 默认是并发安全的——但仅限于它内部的写操作;一旦你用 log.SetOutput 指向一个共享的 *os.File,而这个文件又被多个 goroutine 直接调用 Write,就可能出问题。实际中更常见的是:自己封装了带时间戳/等级的日志函数,又没加锁。
使用场景:HTTP handler、定时任务、协程池中频繁打日志。
-
log.SetOutput后调用log.Print*是安全的,因为标准log包内部用了互斥锁 - 但如果你绕过
log包,直接对文件file.Write(),就必须自己加sync.Mutex - 性能影响:锁粒度越小越好,避免在日志格式化(如拼接字符串)时持有锁
用 lumberjack 轮转日志时注意 MaxSize 单位是字节不是 MB
标准库不支持自动切分日志文件,多数人会引入第三方包 gopkg.in/natefinch/lumberjack.v2。它最常踩的坑是参数理解偏差,尤其是 MaxSize —— 它的单位是字节,不是 MB 或 KB。
错误现象:MaxSize: 100 本意是 100MB,结果日志每 100 字节就切一次;或者设置 MaxBackups: 0 以为能保留全部历史,其实 0 表示“不保留任何备份”,当天日志会被直接删掉。
-
MaxSize建议设为100 * 1024 * 1024(即 100MB),别写100 这种容易看错的表达式 -
MaxAge和MaxBackups是并行生效的,不是“优先级”关系:一个文件既超龄又被轮满,仍会被删 - 轮转发生在写入时判断,不是定时检查,所以低频服务可能长时间不触发轮转
logger := log.New(&lumberjack.Logger{
Filename: "app.log",
MaxSize: 100 * 1024 * 1024, // ← 必须是字节数
MaxBackups: 5,
MaxAge: 28,
}, "", log.LstdFlags)
调试阶段别依赖 log.Printf 查变量,fmt.Printf 更可控
写文件日志时,经常想临时打个 log.Printf("%+v", req) 看结构体内容,结果发现文件里啥也没有——因为 log 默认不输出 panic 堆栈以外的调试信息,而且它自带时间戳和换行,干扰格式解析。
使用场景:开发期快速验证字段值、排查空指针、确认 HTTP 请求体结构。
-
log.Printf会受log.SetFlags影响,比如关掉log.Lshortfile就看不到来源行号 - 调试建议直接用
fmt.Printf+os.Stdout,不走log的输出链路,避免被重定向干扰 - 上线前务必删掉所有
fmt.Printf,它们不会自动消失,也不会被日志级别过滤
日志路径权限、磁盘满、文件系统只读——这些环境问题比代码逻辑更常导致写失败,但错误往往静默发生。检查 log.SetOutput 后是否真有 err == nil,比加一行 log.Println("init ok") 有用得多。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











