go标准库log默认使用全局logger导致日志重复,根本原因是多个模块(如database/sql、net/http)均fallback到同一全局实例并直接写入os.stderr;解决方法是启动时调用log.setoutput(io.discard)禁用默认输出,并显式为各组件(如db.setlogger、server.errorlog)注入自定义logger。

Go 标准库 log 默认使用全局变量 log.Logger,多个模块(尤其是第三方库)直接调用 log.Printf 或未配置自定义 logger 时,会共用同一输出目标——导致同一日志行被打印多次,尤其在引入多个依赖(如 database/sql、net/http、某些 SDK)后非常明显。
为什么 log.Printf 会在不同模块里重复出现
根本原因不是“模块主动打印”,而是它们都 fallback 到了标准库的全局 logger 实例。比如:
-
database/sql在连接超时时会调用log.Print(若未设置DB.SetLogger) -
net/http的Server.ErrorLog默认也指向全局log - 你自己的代码、某个中间件、甚至测试工具(如
testify的某些 debug 模式)也可能悄悄调用log
这些调用不共享上下文,也不协商输出权,全往同一个 os.Stderr 写,自然就叠在一起了。
禁用标准库全局 logger 的输出目标
最直接有效的办法:启动时把全局 log 的输出设为 ioutil.Discard(Go 1.16+ 用 io.Discard),彻底切断默认通路:
import (
"io"
"log"
)
func init() {
log.SetOutput(io.Discard)
}
注意:log.SetOutput 是全局生效的,必须在任何其他模块触发日志前执行(推荐放在 main() 开头或 init() 中)。否则部分日志可能已刷出。
Colly 是一个用于 Go 语言的快速开源爬取和爬虫框架。它适用于从简单的页面提取到异步爬虫处理大量页面集合,支持请求回调和结构化解析。
- 不影响你自定义的
log.New实例(它们有独立输出) - 不影响
logrus、zap等第三方库(它们不依赖标准库log) - 但会静默掉所有未显式配置 logger 的标准库组件日志——这是预期行为,不是 bug
让依赖模块使用你控制的 logger 实例
很多关键组件支持注入自定义 logger,必须显式设置,不能依赖默认行为:
-
database/sql:调用db.SetLogger(yourLogger, 0)(Go 1.21+),其中yourLogger需实现database/sql.LogWriter接口 -
net/http.Server:设置server.ErrorLog = log.New(yourWriter, "[HTTP] ", log.LstdFlags) -
http.Client(某些封装):检查是否暴露WithLogger选项;没有则需 patch 或 wrapper - 第三方 SDK(如 AWS SDK for Go v2):通常通过
middleware.WithLogger注入
重点:不要假设“它用了 log 就会走你的配置”——绝大多数不会。每个可配置点都得手动接上。
用 zap 或 logrus 替换时仍要小心隐式依赖
即使你项目主流程用 zap,只要某个模块内部硬编码了 log.Printf,它依然会打到 os.Stderr(除非你上面已禁用全局输出)。所以:
- 禁用全局
log输出是兜底动作,不可省略 -
logrus默认不接管标准库log,需手动logrus.StandardLogger().Out = ...并重置log.SetOutput,但不如直接丢弃更干净 -
zap完全不碰标准库log,但它的zap.RedirectStdLog只捕获你显式调用的log.*,对第三方库内部调用无效
真正干净的方案永远是:关掉默认出口 + 显式接管所有可配置入口。没有“自动统一”的捷径,漏一个点,重复就还在。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!










