log package in go
本文将简要分析 golang 中自带 log 模块的源码,解析相关知识点并讲解如何将 log 重定向到不同的文件。
相关知识点
- iota 是 constant 定义中的一个 predefiend identifier, 比较有趣,详见 iota
- go 中
|是按位或,||是逻辑或,&也是一样 - append 相关知识
io.Writer是一个 interface, 内部只有一个Write方法的接口,用os.OpenFile打开的文件在特定 flag 下可以看作一个io.Writer- 对 rune, string, byte 的讲解
代码讲解
👇自定义的 constant, 用于指定各种 flag
1 | const ( |
上面的代码 Ldate 到 LUTC 分别为 1, 2, 4, …, 32, 从 bit 的视角来看,它们 1 的位置均不相同,所以通过 | 运算可以得到有意义的消息
👇 Logger 结构体,flag 就是由上面的 constant 组成的,out 可以用于控制输出的位置
1 | type Logger struct { |
通常我们使用 log 模块的时候,直接通过 log.Println 这样的 api 进行打印,原因在于 log 模块中维护了一个默认的 logger std var std = New(os.Stderr, "", LstdFlags), 默认输出在 stderr 中
我们再来看看调用 log.Println 发生了什么
1 | // Println calls Output to print to the standard logger. |
std 就是上文中提到的 log 模块中默认维护的 logger, 再来看看 Output 方法
1 | // Output writes the output for a logging event. The string s contains |
乍一看可能觉得会出现 dead lock, 实际上不会,因为此时的资源只有一个锁,runtime.Caller 不会 hold / 锁住其他资源,所以这里不会有死锁的可能性。这里本质上就是做了一个无锁化的优化
l.buf = l.buf[:0] 的原因没有太懂, 类似于一种把指针移到首位之前的操作,此时 append 并不会 append 在 underlying 数组的末尾,而是直接 append 在第一个地方,把原始数组的第一个元素直接覆盖,这样相当于不用在意之前 buf 到底写了什么,不用特意地清空
l.formatHeader 作用就是在 buf 里面根据我们设置的 prefix 以及相应的 flags 把 Header 填好
最后再把 s 填进去,并且保证末尾一定有一个 \n
我们来具体看看 l.formatHeader,
1 | // formatHeader writes log header to buf in following order: |
第一句话就很值得玩味,我们可能会提出一些疑问
- 如果不传入 buf 的指针可以吗?
- 传入
buf[:0]的作用到底是什么? - append 中
l.prefix...是什么意思?
先回答第一个问题,不可以,因为此时我们要对 slice 进行 append 操作,而 go 中的 append 并不是一个 in-place operation, 会返回一个 new slice,所以必须传入指针
看到这里 buf[:0] 的作用也就有感觉了,相当于返回了一个新的 slice 从头开始,append 会直接从 index 0 开始,直接覆盖之前 slice 下写的东西,不需要自己做一个 clear 的操作
slice append api 中的第二个参数允许我们传入一个 ...T, 也就是我们可以写入任意数目的元素,比如 l = append(l, 1, 2, 3, 4), go 会自动帮我们把 1, 2, 3, 4 转成一个 slice []int{1,2,3,4} 然后传入;我们当然也可以 s = []int{1,2,3}; l = append(l, s...), 会直接把 s 传入并 append. 那么在这里,l.prefix 是一个 string 类型,l.buf 是一个 []byte 类型,那么 l.prefix... 传入的到底是什么呢?
我们不妨先看看 string 和 byte 的关系, 对 rune, string, byte 的讲解,也就是说 string 可以看作一个不可变的 []byte 类型,所以直接 l.prefix... 展开也就是可以理解的了
那么如何写一个输出到固定文件的 logger 呢?
看完 source code 之后,就觉得非常 easy 了,我们可以自己构建 logger 或者把 log.std 的 Output 重定向到其他 io.Writer 即可,打开一个固定的文件就能做到这一点, 因为 os.OpenFile 返回的 *File 类型有实现 io.Writer 的 interface
比如
1 | log.SetPrefix(prefix + " ") |