log.Println 和 log.Printf 的换行行为不同:前者自动加换行符,后者严格按格式输出,需显式添加\n;SetOutput 必须在首次 log.Print* 前调用;多级别日志应使用多个 log.New 实例;文件日志必须配置轮转。

log.Println 和 log.Printf 的换行行为差异必须搞清
很多人写 log.Printf 时漏掉换行符,结果多条日志挤在一行,排查时根本分不清哪条属于哪个请求。这不是 bug,是设计如此:log.Print 和 log.Println 内部自动加 \n;log.Printf 则严格按格式字符串输出,不额外补换行。
- 错误写法:
log.Printf("failed to connect: %v", err)→ 后续log.Println("shutting down")可能连在一行 - 正确做法:显式加
\n,如log.Printf("failed to connect: %v\n", err),或统一用log.Println(fmt.Sprintf("...")) - 生产环境建议避免裸用
log.Printf,尤其在错误路径中——容易因换行缺失导致日志解析失败
SetOutput 必须在首次 log.Print* 调用前设置
想把日志写进文件却还是打到终端?大概率是 log.SetOutput 调晚了。Go 的标准 log 包只在第一次调用 log.Print* 时绑定输出目标,之后再调 SetOutput 对已初始化的内部 logger 无效。
- 安全顺序:先
os.OpenFile→ 检查err→log.SetOutput(file)→ 再调log.Println - 如果项目里已大量使用顶层函数(如
log.Fatal),别试图后期重定向——要么重构为传参式*log.Logger,要么一开始就设好 - 注意:
log.SetOutput只影响全局 logger,不影响你用log.New创建的独立实例
用多个 log.New 实例模拟日志级别最稳妥
标准库没有 Info/Error 方法,但硬套一个“封装结构体”反而增加维护成本。直接为每个级别建一个 *log.Logger 实例,简单、清晰、无隐藏副作用。
- 示例:
InfoLog := log.New(os.Stdout, "[INFO] ", log.LstdFlags|log.Lshortfile),ErrorLog := log.New(os.Stderr, "[ERROR] ", log.LstdFlags|log.Lshortfile) - 错误日志写
os.Stderr是惯例,运维工具(如 systemd、supervisor)会自动区分 stderr/stdout 流 - 不要给所有级别共用同一个文件句柄再靠前缀分流——看似省事,实则丢失错误隔离能力(比如磁盘满时 error 日志可能写不进去,info 还在狂刷)
文件日志必须配轮转,否则迟早填满磁盘
标准库连最基础的按大小切割都不支持,长期运行的服务不加轮转,app.log 几天就能涨到几十 GB。别信“先上线再加”的说法——上线当天就该配好。
立即学习“go语言免费学习笔记(深入)”;
- 推荐用
gopkg.in/natefinch/lumberjack.v2,三行代码搞定:log.SetOutput(&lumberjack.Logger{Filename: "logs/app.log", MaxSize: 10, MaxBackups: 5}) - 切忌自己用
time.Now().Format("2006-01-02")拼文件名做“每日轮转”——没锁、没原子重命名、goroutine 并发时极易冲突或丢日志 - 如果用
log.SetOutput指向文件,记得defer file.Close(),否则程序异常退出时最后几条日志可能缓存未刷盘
标准库 log 不是不能用,而是它的“简单”有明确边界:不处理轮转、不隔离级别、不防换行遗漏。越早接受这些限制,越少在凌晨三点翻查被挤成一团的日志文件。










