不能,log.setoutput只接受单个io.writer;需用io.multiwriter实现同步多写但无法分级,或改用zap/zerolog等支持级别分发的结构化日志库。

log.SetOutput 无法同时写入多个目标?
Go 标准库的 log.SetOutput 只接受单个 io.Writer,直接传入多个 writer 会编译失败。这不是设计缺陷,而是标准库刻意保持简单——它不负责分流,分流得你自己做。
常见错误是试图用 io.MultiWriter(os.Stdout, file) 粗暴拼接,看似能工作,但会导致控制台输出延迟(因默认文件 writer 带缓冲)、panic 时日志丢失、以及无法为不同输出定制格式(比如控制台要带颜色,文件要带毫秒时间戳)。
- 必须用
io.MultiWriter时,确保所有 writer 都已正确打开且非 nil,否则Write调用会静默失败 - 文件 writer 推荐用
os.OpenFile(..., os.O_CREATE|os.O_APPEND|os.O_WRONLY),避免每次写都重开文件 -
io.MultiWriter是并发安全的,但底层 writer 自身需保证线程安全(如os.File是安全的)
用 zap 或 zerolog 替代标准 log 更实际
标准 log 包不支持多目标 + 不同编码 + 动态级别,硬改成本高。生产环境推荐直接上结构化日志库,zap 是最常用选择,启动快、内存低、支持同步/异步写入。
示例:同时输出 JSON 到文件 + 控制台(带颜色):
logger := zap.New(zapcore.NewTee(
zapcore.NewCore(
zapcore.NewJSONEncoder(zapcore.EncoderConfig{...}),
zapcore.AddSync(&os.File{Fd: int(1)}), // stdout
zapcore.InfoLevel,
),
zapcore.NewCore(
zapcore.NewJSONEncoder(zapcore.EncoderConfig{...}),
zapcore.AddSync(zapcore.Lock(os.Stderr)), // 实际应为文件句柄
zapcore.DebugLevel,
),
))
- 注意两个
EncoderConfig可以不同:控制台用ConsoleEncoder,文件用JSONEncoder -
zapcore.Lock必须包裹文件 writer,防止并发写乱序 - 不要用
zap.NewDevelopment生产环境,它默认加了 caller 和 stacktrace,性能损耗大
自己封装 multiWriter 支持不同格式和级别
如果坚持用标准 log,又需要差异化输出(比如控制台只打 info+warn,文件记录 debug+all),就得自己实现一个带路由逻辑的 writer。
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
核心是实现 io.Writer 接口,并在 Write([]byte) 中解析日志行(或前置加前缀标识级别),再按规则分发:
type MultiLevelWriter struct {
console io.Writer
file io.Writer
level string // "info", "debug" 等,由调用方注入
}
func (w *MultiLevelWriter) Write(p []byte) (n int, err error) {
if strings.Contains(string(p), "[DEBUG]") && w.level == "debug" {
w.file.Write(p)
}
if !strings.Contains(string(p), "[DEBUG]") {
w.console.Write(p)
}
return len(p), nil
}
- 这种字符串匹配方式脆弱,真实项目建议用结构化前缀(如
[INFO][2024-05-20T10:30:00Z]) - 别在
Write里做耗时操作(如打开文件、网络请求),否则阻塞整个日志流 - 控制台 writer 用
os.Stdout即可,无需额外包装;文件 writer 一定要用bufio.NewWriter缓冲,否则 I/O 毛刺明显
文件日志轮转不能靠手动判断时间
多输出场景下,轮转(rotate)最容易被忽略。有人写个 goroutine 定时检查文件大小然后 rename,结果导致正在写的文件被移走,新日志写进空文件,旧日志残留没归档。
正确做法是用成熟轮转库,比如 lumberjack.Logger:
fileWriter := &lumberjack.Logger{
Filename: "app.log",
MaxSize: 100, // MB
MaxBackups: 5,
MaxAge: 28, // days
Compress: true,
}
-
lumberjack内部用os.Rename+os.OpenFile原子切换,不会丢日志 - 它不是
io.Writer的完整实现,但实现了Write和Close,可直接传给log.SetOutput或zapcore.AddSync - 别把
lumberjack.Logger和log.Logger混淆——前者只是 writer,后者才是日志实例
多输出时每个目标都要独立配置轮转策略,控制台不需要轮转,文件必须配,这是最常漏掉的一环。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!










