原文地址:Golang 源碼剖析:log 標準庫git
2018/09/28 20:03:08 EDDYCJY Blog...
[日期]<空格>[時分秒]<空格>[內容]<n>github
type Logger struct { mu sync.Mutex prefix string flag int out io.Writer buf []byte }
(1) mu:互斥鎖,用於確保原子的寫入
(2) prefix:每行需寫入的日誌前綴內容
(3) flag:設置日誌輔助信息(時間、文件名、行號)的寫入。可選以下標識位:golang
const ( Ldate = 1 << iota // value: 1 Ltime // value: 2 Lmicroseconds // value: 4 Llongfile // value: 8 Lshortfile // value: 16 LUTC // value: 32 LstdFlags = Ldate | Ltime // value: 3 )
(4) out:io.Writer
(5) buf:用於存儲將要寫入的日誌內容app
func New(out io.Writer, prefix string, flag int) *Logger { return &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") )
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) } ...
這一部分介紹最經常使用的日誌寫入方法,從源碼可得知 Xrintln
、Xrintf
函數 換行、可變參數都是經過 fmt
標準庫的方法去實現的ui
Fatal
和 Panic
是經過 os.Exit(1)
、panic(s)
集成實現的。而具體的組裝邏輯是經過 Output
方法實現的this
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&(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(&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
,將自動補全一個換行日誌
func (l *Logger) formatHeader(buf *[]byte, t time.Time, file string, line int) { *buf = append(*buf, l.prefix...) if l.flag&(Ldate|Ltime|Lmicroseconds) != 0 { if l.flag&LUTC != 0 { t = t.UTC() } if l.flag&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&(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&Lmicroseconds != 0 { *buf = append(*buf, '.') itoa(buf, t.Nanosecond()/1e3, 6) } *buf = append(*buf, ' ') } } if l.flag&(Lshortfile|Llongfile) != 0 { if l.flag&Lshortfile != 0 { short := file for i := len(file) - 1; i > 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, ": "...) } }
該方法主要是用於格式化日誌頭(前綴),根據入參不一樣的標識位,添加分隔符和對應的值到日誌信息中。執行流程以下:code
(1)若是不是空值,則將 prefix 寫入 buform
(2)若是設置 Ldate
、Ltime
、Lmicroseconds
,則對應將日期和時間寫入 buf
(3)若是設置 Lshortfile
、Llongfile
,則對應將文件和行號信息寫入 buf
func itoa(buf *[]byte, i int, wid int) { // Assemble decimal in reverse order. var b [20]byte bp := len(b) - 1 for i >= 10 || wid > 1 { wid-- q := i / 10 b[bp] = byte('0' + i - q*10) bp-- i = q } // i < 10 b[bp] = byte('0' + i) *buf = append(*buf, b[bp:]...) }
該方法主要用於將整數轉換爲定長的十進制 ASCII,同時給出負數寬度避免左側補 0。另外會以相反的順序組合十進制
在標準庫內,可經過其開放的 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
進行操做(以此解決原子問題),其他邏輯均爲組裝日誌信息和轉換數值格式,該包較爲經典,能夠多讀幾遍 😄
爲何在調用 runtime.Caller
前要先解鎖,後再加鎖呢?