
为什么不用 log 包直接写多格式日志
Go 标准 log 包是单例、全局、固定输出格式的——你调一次 log.SetOutput,所有日志就都走那个地方;想同时输出 JSON 和 plain text?它不支持;想按模块切换格式?得自己包一层。工厂模式不是为了炫技,是为了解耦「日志行为」和「日志实现」,让 Logger 接口背后可以自由替换 JSONLogger、PlainLogger、甚至 CloudWatchLogger。
常见错误现象:log.SetFlags(0) 后发现时间戳没了,但其他格式(比如字段顺序、换行)还是改不了;或者硬编码多个 if format == "json" { ... },一加新格式就得改主逻辑。
- 工厂的核心是返回满足
Logger接口的实例,不是返回字符串或配置 - 不要在工厂函数里做 I/O 初始化(如打开文件),那属于具体实现的职责
- 避免把格式选择逻辑(如
env == "prod")写死在工厂内部,应作为参数传入
NewLogger 工厂函数怎么设计才不踩坑
工厂函数名不一定要叫 NewLogger,但必须清晰表达「构造一个可运行的日志器」。关键不是返回 *log.Logger,而是返回自定义接口实例——否则工厂失去意义。
示例接口定义:
立即学习“go语言免费学习笔记(深入)”;
type Logger interface {
Info(string, ...any)
Error(string, ...any)
WithField(string, any) Logger
}
实操建议:
- 工厂函数接收
format string和io.Writer两个必要参数,其余(如 level、traceID 注入)用选项函数(Option)传入,避免参数爆炸 - 对未知
format值(如"xml")应 panic 或返回 error,别静默 fallback 到 plain —— 这会让配置错误难以发现 - 不要在工厂里调
log.SetOutput,那是全局副作用,和工厂「创建独立实例」的本意冲突
JSON 日志和文本日志的底层差异在哪
不是「加个 json.Marshal」就叫 JSON 日志。真实场景中,JSONLogger 要处理字段扁平化、时间格式统一(RFC3339)、错误对象序列化、以及避免 panic(比如日志内容含 nan 或 unexported struct 字段)。
而 PlainLogger 的重点是可读性:字段对齐、颜色支持(终端)、截断超长消息。两者共用同一套 Info/Error 方法签名,但内部格式化逻辑完全隔离。
-
JSONLogger应使用json.Encoder而非json.Marshal+fmt.Fprint,减少内存分配 - 文本日志若要支持 color,需检测
os.Stdout是否为终端(用isatty.IsTerminal),不能无条件输出 ANSI 码 - 两者都应把
time.Now()提前取好,避免不同字段时间不一致(尤其在高并发打日志时)
如何让工厂支持动态重载日志格式
线上服务常需不重启切日志格式(比如 debug 时临时开 JSON)。这要求工厂返回的 Logger 实例本身支持运行时变更,而不是每次调用工厂新建一个。
可行做法是让具体实现持有一个原子指针(*atomic.Value)指向当前格式化器,工厂只负责初始化这个指针。
- 不要用互斥锁保护整个
Info方法——性能杀手;锁只用于更新格式器指针 - 重载时新旧格式器可能并存一小段时间,确保新格式器初始化完成后再替换指针
- 如果用了结构体嵌入(如
BaseLogger),注意字段赋值顺序,避免指针替换后部分字段仍引用旧状态
真正难的不是写工厂,是让每个具体日志器既保持轻量,又能在 runtime 安全切换上下文。格式只是表象,生命周期和状态归属才是容易被忽略的复杂点。










