最近闲的没事干,笔者自己又独立地写了个日志切分的库(文档链接)。在这里记录一下要点。
并发问题
标准库的 log.Logger 的注释,源码链接:
A Logger represents an active logging object that generates lines of output to an io.Writer. Each logging operation makes a single call to the Writer's Write method. A Logger can be used simultaneously from multiple goroutines; it guarantees to serialize access to the Writer.
翻译一下最后一句话:“Logger 可以同时被多个协程使用;它保证串行地访问 Writer。”。其言下之意就是,Logger 会加锁保护 Writer。所以如果自己实现 io.Writer 的话,不需要使用锁或 atomic。slog 也是一样,虽然我没找到相应的文档,但在实现中也给 Writer 加锁了。源码链接
不过如果想要开发一个 io.WriterCloser 的话,事情就又不一样了。例如下面的代码在执行 go test -race .
时会报数据竞态的错误:
func TestClose_in_another_goroutine(t *testing.T) {
var wc io.WriteCloser = new(writeCloser)
logger := log.New(wc, "", 0)
go logger.Println("foo")
wc.Close()
}
type writeCloser struct {
n int
}
func (wc *writeCloser) Write(p []byte) (int, error) {
wc.n = 1 // set state
return len(p), nil
}
func (wc *writeCloser) Close() error {
_ = wc.n // get state
return nil
}
因为 Write 和 Close 是在不同的协程中调用的,且它们操作了同一个数据,所以会发生数据竞争错误。
其他
不过说实在的,工程要是容器化了的话就可以不用切分日志了。日志直接打到 stdout 上,然后让 docker 或 k8s 去搞日志切分的事情。比如 docker compose 就可以这样写,文档链接:
# compose.yaml
service:
app:
image: xxx
# 日志配置
logging:
driver: json-file
options:
max-size: 10m
max-file: 10