Golang 源碼剖析:log 標準庫

Golang 源碼剖析:log 標準庫

原文地址:Golang 源碼剖析:log 標準庫git

日誌

輸出

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

構成

[日期]<空格>[時分秒]<空格>[內容]<n>github

源碼剖析

Logger

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
)
  • 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:用於存儲將要寫入的日誌內容app

New

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")
)
  • 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 標準庫的方法去實現的ui

FatalPanic 是經過 os.Exit(1)panic(s) 集成實現的。而具體的組裝邏輯是經過 Output 方法實現的this

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&(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,將自動補全一個換行日誌

Logger.formatHeader

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)若是設置 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 >= 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。另外會以相反的順序組合十進制

如何定製化 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 進行操做(以此解決原子問題),其他邏輯均爲組裝日誌信息和轉換數值格式,該包較爲經典,能夠多讀幾遍 😄

問題

爲何在調用 runtime.Caller 前要先解鎖,後再加鎖呢?

相關文章
相關標籤/搜索