Go标准log包默认缓冲写入,需显式Sync()或用自动刷新Writer;轮转推荐rotatelogs;并发写需线程安全io.Writer;JSON日志应换用zerolog/zap;须预检路径权限与磁盘空间。

用 log 包写入文件时,为什么日志没刷到磁盘?
Go 标准库的 log 默认使用带缓冲的 io.Writer,如果直接传入 os.File 而不显式调用 Sync(),程序崩溃或提前退出时容易丢失最后几条日志。
- 务必用
os.O_CREATE | os.O_WRONLY | os.O_APPEND打开文件,避免覆盖历史日志 - 写入后手动调用
file.Sync()(尤其在关键错误日志后) - 更稳妥的做法是封装一个带自动刷新的
Writer,比如用bufio.NewWriterSize(file, 4096)并在每次Write()后Flush()
file, _ := os.OpenFile("app.log", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644)
logger := log.New(file, "[INFO] ", log.LstdFlags|log.Lshortfile)
// 记录后立即刷盘
logger.Println("startup completed")
file.Sync()
需要轮转日志(logrotate)时,fsnotify 和 rotatelogs 怎么选?
标准 log 包不支持自动切分,必须借助第三方。常见方案有两个:自己监听 SIGHUP 或文件变更,或用成熟轮转库。
-
lestrrat-go/file-rotatelogs是目前最稳定的选项,支持按时间/大小切分、保留份数、压缩归档 -
fsnotify适合已有信号处理机制的项目(如监听SIGHUP后手动Reopen()),但需自行管理文件句柄和并发安全 - 注意:轮转期间若未正确关闭旧文件句柄,会导致
too many open files错误
rl, _ := rotatelogs.New(
"logs/app.%Y%m%d.log",
rotatelogs.WithMaxAge(7*24*time.Hour),
rotatelogs.WithRotationCount(30),
)
logger := log.New(rl, "", log.LstdFlags)
多 goroutine 写日志出现乱序或截断,怎么加锁才不影响性能?
log.Logger 本身不是并发安全的——它的 Output() 方法内部不加锁,多个 goroutine 直接调用会竞争写入缓冲区。
- 最简单:用
sync.Mutex包一层Write(),但高并发下会成为瓶颈 - 推荐用
log.SetOutput()替换为线程安全的io.Writer,例如lumberjack.Logger或自定义带 channel 的 writer - 避免在日志中拼接大量字符串(如
fmt.Sprintf),应让log.Printf自己格式化,减少临时内存分配
var mu sync.Mutex
logger := log.New(os.Stdout, "", log.LstdFlags)
// 不要这样:
go func() { mu.Lock(); defer mu.Unlock(); logger.Println("msg") }()
// 更好:用 lumberjack 或直接设为 sync.Once + buffered channel writer
想输出 JSON 日志给 ELK,但标准 log 包不支持结构化,怎么办?
标准 log 只能输出纯文本,JSON 字段必须手动构造,且无法统一控制字段名(如 level、timestamp)。硬编码易出错,也不利于后续解析。
立即学习“go语言免费学习笔记(深入)”;
- 用
zerolog或zap替代——它们原生支持结构化日志、字段类型校验、采样和 hook - 若必须用标准库,可封装一个
JSONLogger类型,实现io.Writer接口,在Write()中解析每行、补全 JSON 字段再写入 - 注意时间戳精度:
time.Now().UTC().Format(time.RFC3339Nano)比log.Lmicroseconds更适配 ES
logger := zerolog.New(os.Stderr).With().Timestamp().Logger()
logger.Info().Str("component", "api").Int("status", 200).Msg("request completed")
日志路径权限、磁盘满、时区配置这些看似外围的问题,往往比逻辑错误更难定位。写入前先 os.Stat() 检查目录可写性,轮转时用 syscall.Getpid() 做文件名防冲突,比事后排查快得多。










