多个 init() 函数会互相覆盖日志配置,因各包独立执行 init 且对同一全局变量重复赋值;应统一在 main 中调用 setup 初始化,其他模块只使用导出的 logger 实例。

为什么多个 init() 函数会互相覆盖日志配置
Go 程序启动时,所有包的 init() 函数按导入顺序执行,且每个包只执行一次。但如果你在多个模块(比如 pkg/log、service/auth、infra/db)里各自写了 log.Init() 或直接调用 zap.NewProduction() 初始化全局 logger,就会出现后初始化的覆盖前者的现象——不是并发冲突,而是纯粹的变量赋值覆盖。
典型表现是:本地调试时日志格式正常,一打包进 Docker 就丢失字段或级别变回 INFO;或者某个服务启用了 debug 模式,但另一个模块悄悄把 logger 换成了生产配置。
- 根本原因:多个地方对同一个全局变量(如
log.Logger)做了重复赋值 - 常见诱因:各模块为“自包含”而自行初始化日志,没考虑主程序统一管控
- 陷阱:用
sync.Once包裹初始化也不够——如果每个包都声明自己的once sync.Once,那还是各自独立执行
如何让日志实例只被初始化一次且可跨模块访问
必须把初始化逻辑收归一处,且确保其他模块只使用、不初始化。推荐做法是定义一个显式初始化入口,并通过导出变量暴露 logger,而非依赖 init()。
- 在主模块(如
cmd/myapp/main.go)中调用log.Setup(cfg),且仅在此处调用 - 日志包导出的是
log.Logger(*zap.Logger 类型),而不是log.Init()这类副作用函数 - 其他模块一律用
import "yourapp/pkg/log"后直接用log.Logger.Info(...),禁止调用任何初始化函数 - 若需不同 logger(如 DB 模块要单独字段),应通过
log.Logger.With(...)派生,而非新建全局实例
示例:
// pkg/log/log.go
var Logger *zap.Logger
func Setup(cfg Config) error {
l, err := zap.NewProduction(zap.AddCaller())
if err != nil {
return err
}
Logger = l
return nil
}
模块间 logger 配置不一致时怎么快速定位
当发现某处日志输出格式异常,不要猜哪个包搞坏了,直接加一行诊断代码:
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
fmt.Printf("logger addr: %p, level: %v\n", log.Logger, log.Logger.Core().Level())
- 地址相同说明是同一实例;地址不同,说明有模块偷偷 new 了新 logger 并赋值给全局变量
- 检查所有 import 路径下是否含
log.Init、zap.New、log.SetOutput等调用 - 特别注意 vendor 或 replace 引入的第三方包——有些 SDK 会在 init 里强行初始化自己的 logger
- 用
go mod graph | grep log查依赖树,确认是否意外引入了多个日志封装包
为什么不能靠 init() + sync.Once 解决
因为 Go 的 init() 是按包执行的,每个包都有独立作用域。你在 pkg/log 里写:
var once sync.Once
func init() {
once.Do(func() { Logger = zap.NewNop() })
} 这个 once 只对当前包有效。如果 service/user 也写了同样的代码,它的 once 和 pkg/log 的 once 完全无关,照样会执行自己的初始化。
-
sync.Once无法跨包共享状态 - 多个包的
init()执行顺序由构建时导入图决定,不可控 - 一旦有任意一个包在主程序之前执行了初始化,就可能被后续包覆盖
真正可控的初始化时机只有一个:main 函数开始后、业务逻辑启动前。
最易被忽略的一点:即使你严格控制了初始化入口,仍要检查所有 defer log.Logger.Sync() 是否只在 main 里调用一次——重复 Sync 可能导致 panic 或丢日志。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!










