首页 技术 正文
技术 2022年11月14日
0 收藏 414 点赞 4,348 浏览 3958 个字

Golang 源码剖析:log 标准库

原文地址:Golang 源码剖析:log 标准库

日志

输出


2018/09/28 20:03:08 EDDYCJY Blog...

构成

[日期]<空格>[时分秒]<空格>[内容]<n>

源码剖析

Logger


type Logger struct {
mu sync.Mutex
prefix string
flag int
out io.Writer
buf []byte
}

(1) mu:互斥锁,用于确保原子的写入
(2) prefix:每行需写入的日志前缀内容
(3) flag:设置日志辅助信息(时间、文件名、行号)的写入。可选如下标识位:


const (
Ldate = 1 &lt;&lt; iota // value: 1
Ltime // value: 2
Lmicroseconds // value: 4
Llongfile // value: 8
Lshortfile // value: 16
LUTC // value: 32
LstdFlags = Ldate | Ltime // value: 3
)
  • Ldate:当地时区的格式化日期:2009/01/23
  • Ltime:当地时区的格式化时间:01:23:23
  • Lmicroseconds:在 Ltime 的基础上,增加微秒的时间数值显示
  • Llongfile:完整的文件名和行号:/a/b/c/d.go:23
  • Lshortfile:当前文件名和行号:d.go:23,会覆盖 Llongfile 标识
  • LUTC:如果设置 Ldate 或 Ltime,且设置 LUTC,则优先使用 UTC 时区而不是本地时区
  • LstdFlags:Logger 的默认初始值(Ldate 和 Ltime)

(4) out:io.Writer
(5) buf:用于存储将要写入的日志内容

New


func New(out io.Writer, prefix string, flag int) *Logger {
return &amp;Logger{out: out, prefix: prefix, flag: flag}
}var std = New(os.Stderr, "", LstdFlags)

New 方法用于初始化 Logger,接受三个初始参数,可以定制化而在 log 包内默认会初始一个 std,它指向标准输入流。而默认的标准输出、标准错误就是显示器(输出到屏幕上),标准输入就是键盘。辅助的时间信息默认为 Ldate | Ltime,也就是 2009/01/23 01:23:23


// os
var (
Stdin = NewFile(uintptr(syscall.Stdin), "/dev/stdin")
Stdout = NewFile(uintptr(syscall.Stdout), "/dev/stdout")
Stderr = NewFile(uintptr(syscall.Stderr), "/dev/stderr")
)
  • Stdin:标准输入
  • Stdout:标准输出
  • Stderr:标准错误

Getter

  • Flags
  • Prefix

Setter

  • SetFlags
  • SetPrefix
  • SetOutput

Print.., Fatal.., Panic..


func Print(v ...interface{}) {
std.Output(2, fmt.Sprint(v...))
}func Printf(format string, v ...interface{}) {
std.Output(2, fmt.Sprintf(format, v...))
}func Println(v ...interface{}) {
std.Output(2, fmt.Sprintln(v...))
}func Fatal(v ...interface{}) {
std.Output(2, fmt.Sprint(v...))
os.Exit(1)
}func Panic(v ...interface{}) {
s := fmt.Sprint(v...)
std.Output(2, s)
panic(s)
}...

这一部分介绍最常用的日志写入方法,从源码可得知 XrintlnXrintf 函数 换行可变参数都是通过 fmt 标准库的方法去实现的

FatalPanic 是通过 os.Exit(1)panic(s) 集成实现的。而具体的组装逻辑是通过 Output 方法实现的

Logger.Output


func (l *Logger) Output(calldepth int, s string) error {
now := time.Now() // get this early.
var file string
var line int
l.mu.Lock()
defer l.mu.Unlock()
if l.flag&amp;(Lshortfile|Llongfile) != 0 {
// Release lock while getting caller info - it's expensive.
l.mu.Unlock()
var ok bool
_, file, line, ok = runtime.Caller(calldepth)
if !ok {
file = "???"
line = 0
}
l.mu.Lock()
}
l.buf = l.buf[:0]
l.formatHeader(&amp;l.buf, now, file, line)
l.buf = append(l.buf, s...)
if len(s) == 0 || s[len(s)-1] != '\n' {
l.buf = append(l.buf, '\n')
}
_, err := l.out.Write(l.buf)
return err
}

Output 方法,简单来讲就是将写入的日志事件信息组装并输出,它会根据 flag 标识位的不同来使用 runtime.Caller 去获取当前 goroutine 所执行的函数文件、行号等调用信息(log 标准库中默认深度为 2)。另外如果结尾不是换行符 \n,将自动补全一个换行

Logger.formatHeader


func (l *Logger) formatHeader(buf *[]byte, t time.Time, file string, line int) {
*buf = append(*buf, l.prefix...)
if l.flag&amp;(Ldate|Ltime|Lmicroseconds) != 0 {
if l.flag&amp;LUTC != 0 {
t = t.UTC()
}
if l.flag&amp;Ldate != 0 {
year, month, day := t.Date()
itoa(buf, year, 4)
*buf = append(*buf, '/')
itoa(buf, int(month), 2)
*buf = append(*buf, '/')
itoa(buf, day, 2)
*buf = append(*buf, ' ')
}
if l.flag&amp;(Ltime|Lmicroseconds) != 0 {
hour, min, sec := t.Clock()
itoa(buf, hour, 2)
*buf = append(*buf, ':')
itoa(buf, min, 2)
*buf = append(*buf, ':')
itoa(buf, sec, 2)
if l.flag&amp;Lmicroseconds != 0 {
*buf = append(*buf, '.')
itoa(buf, t.Nanosecond()/1e3, 6)
}
*buf = append(*buf, ' ')
}
}
if l.flag&amp;(Lshortfile|Llongfile) != 0 {
if l.flag&amp;Lshortfile != 0 {
short := file
for i := len(file) - 1; i &gt; 0; i-- {
if file[i] == '/' {
short = file[i+1:]
break
}
}
file = short
}
*buf = append(*buf, file...)
*buf = append(*buf, ':')
itoa(buf, line, -1)
*buf = append(*buf, ": "...)
}
}

该方法主要是用于格式化日志头(前缀),根据入参不同的标识位,添加分隔符和对应的值到日志信息中。执行流程如下:

(1)如果不是空值,则将 prefix 写入 buf

(2)如果设置 LdateLtimeLmicroseconds,则对应将日期和时间写入 buf

(3)如果设置 LshortfileLlongfile,则对应将文件和行号信息写入 buf

Logger.itoa


func itoa(buf *[]byte, i int, wid int) {
// Assemble decimal in reverse order.
var b [20]byte
bp := len(b) - 1
for i &gt;= 10 || wid &gt; 1 {
wid--
q := i / 10
b[bp] = byte('0' + i - q*10)
bp--
i = q
}
// i &lt; 10
b[bp] = byte('0' + i)
*buf = append(*buf, b[bp:]...)
}

该方法主要用于将整数转换为定长的十进制 ASCII,同时给出负数宽度避免左侧补 0。另外会以相反的顺序组合十进制

如何定制化 Logger

在标准库内,可通过其开放的 New 方法来实现各种各样的自定义 Logger 组件,但是为什么也可以直接 log.Print* 等方法呢?


func New(out io.Writer, prefix string, flag int) *Logger

其实是在标准库内,如果你刚刚细心的看了前面的小节,不难发现其默认实现了一个 Logger 组件


var std = New(os.Stderr, "", LstdFlags)

这也是一个小小的精妙之处 ⭕️

总结

通过查阅 log 标准库的源码,可得知最简单的一个日志包应该如何编写。另外 log 包是在所有涉及到 Logger 的地方都对 sync.Mutex 进行操作(以此解决原子问题),其余逻辑均为组装日志信息和转换数值格式,该包较为经典,可以多读几遍

相关推荐
python开发_常用的python模块及安装方法
adodb:我们领导推荐的数据库连接组件bsddb3:BerkeleyDB的连接组件Cheetah-1.0:我比较喜欢这个版本的cheeta…
日期:2022-11-24 点赞:878 阅读:9,495
Educational Codeforces Round 11 C. Hard Process 二分
C. Hard Process题目连接:http://www.codeforces.com/contest/660/problem/CDes…
日期:2022-11-24 点赞:807 阅读:5,909
下载Ubuntn 17.04 内核源代码
zengkefu@server1:/usr/src$ uname -aLinux server1 4.10.0-19-generic #21…
日期:2022-11-24 点赞:569 阅读:6,742
可用Active Desktop Calendar V7.86 注册码序列号
可用Active Desktop Calendar V7.86 注册码序列号Name: www.greendown.cn Code: &nb…
日期:2022-11-24 点赞:733 阅读:6,496
Android调用系统相机、自定义相机、处理大图片
Android调用系统相机和自定义相机实例本博文主要是介绍了android上使用相机进行拍照并显示的两种方式,并且由于涉及到要把拍到的照片显…
日期:2022-11-24 点赞:512 阅读:8,134
Struts的使用
一、Struts2的获取  Struts的官方网站为:http://struts.apache.org/  下载完Struts2的jar包,…
日期:2022-11-24 点赞:671 阅读:5,298