[go] 基于 io.Writer 开发日志切分功能的要点

30 阅读1分钟

最近闲的没事干,笔者自己又独立地写了个日志切分的库(文档链接)。在这里记录一下要点。

并发问题

标准库的 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