io.Writer装饰器不能仅靠嵌入struct实现,因日志库常探测io.Closer、WriteString等接口;必须显式实现Write、Close、WriteString等方法,并用atomic保证并发安全,同时正确处理空写、部分写及panic场景。

为什么 io.Writer 装饰器不能直接套一层 struct 就完事
因为 io.Writer 接口只定义了 Write([]byte) (int, error),但实际日志库(比如 log.SetOutput 或 zapcore.AddSync)常会检查底层是否实现了 io.Closer、io.StringWriter 甚至 WriteString 方法。如果只嵌入 io.Writer 字段却不透传这些方法,调用方一调用 Close() 就 panic,或者写字符串时退化成 []byte 转换,性能掉一截。
- 必须显式实现所有可能被日志库探测的接口:至少
Write、Close(如果底层支持)、WriteString - 别用匿名字段“自动提升”——Go 不会自动把嵌入字段的方法提升为当前类型的方法,除非你明确写出来
- 统计逻辑必须放在
Write和WriteString里,否则漏计流量(比如 zap 默认优先走WriteString)
怎么让装饰器同时统计字节数又不破坏原有行为
核心是把原始 io.Writer 包一层,每次写都先累加长度,再原样转发。但要注意:返回值里的 int 是实际写出字节数,不是你统计的值;出错时也得原样返回错误,不能吞掉。
type CountingWriter struct {
w io.Writer
bytes uint64
}
func (c *CountingWriter) Write(p []byte) (int, error) {
n, err := c.w.Write(p)
atomic.AddUint64(&c.bytes, uint64(n))
return n, err
}
func (c *CountingWriter) WriteString(s string) (int, error) {
n, err := c.w.WriteString(s)
atomic.AddUint64(&c.bytes, uint64(n))
return n, err
}
func (c *CountingWriter) Close() error {
if closer, ok := c.w.(io.Closer); ok {
return closer.Close()
}
return nil
}
func (c *CountingWriter) Bytes() uint64 {
return atomic.LoadUint64(&c.bytes)
}
- 用
atomic是因为日志往往是多 goroutine 并发写的,uint64非原子读写在 32 位系统上会出错 - 不要在
Write里做格式化或缓冲——那属于业务逻辑,装饰器只负责“路过计数” - 如果底层
w不是io.Closer,Close()返回nil比 panic 更安全
在 zap / log / slog 里怎么安全注入这个装饰器
不同日志库对 io.Writer 的使用方式不同:标准库 log 只认 Write;zap 会尝试转成 io.Writer 再检查 io.Closer 和 WriteString;slog(Go 1.21+)默认用 slog.NewTextHandler 或 slog.NewJSONHandler,它们内部包装了 io.Writer,但不会主动调用 Close,除非你传的是 *os.File 这类可关对象。
- 给
log.SetOutput:直接传&CountingWriter{w: os.Stderr}即可 - 给
zapcore.AddSync:传zapcore.AddSync(&CountingWriter{w: writer}),确保writer本身支持WriteString(比如os.Stderr支持,bytes.Buffer不支持) - 给
slog.NewTextHandler:构造 handler 后,它内部会调用Write,所以装饰器有效;但别指望它调Close,除非你自己 wrap 的是*os.File
容易被忽略的边界:空写、部分写、panic 场景
真实日志流里常有 Write([]byte{})(空切片)、网络 writer 返回 n (部分写)、甚至底层 writer 在写过程中 panic(比如 pipe 关闭)。装饰器如果没处理好,统计就错,甚至导致整个日志挂死。
立即学习“go语言免费学习笔记(深入)”;
-
Write([]byte{})必须返回(0, nil),且不增加计数——否则空日志也会涨字节数 - 部分写场景下,只累加
n,不是len(p);否则高估流量 - 如果底层
Writepanic,装饰器不能 recover——日志库需要知道失败,强行 recover 会导致错误静默 - 别在
Bytes()方法里加锁,用atomic.LoadUint64就够;但如果你要重置计数,就得用atomic.StoreUint64,别用赋值










