1. 程式人生 > >Golang學習筆記--log模組(二)

Golang學習筆記--log模組(二)

前一篇文章我們看到了Golang標準庫中log模組的使用,那麼它是如何實現的呢?下面我從log.Logger開始逐步分析其實現。 其原始碼可以參考官方地址

1.Logger結構

首先來看下型別Logger的定義:

type Logger struct {
    mu     sync.Mutex // ensures atomic writes; protects the following fields
    prefix string     // prefix to write at beginning of each line
    flag   int        // properties
    out    io.Writer  // destination for output
    buf    []byte     // for accumulating text to write
}

主要有5個成員,其中3個我們比較熟悉,分別是表示Log字首的 "prefix",表示Log頭標籤的 "flag" ,以及Log的輸出目的地out。 buf是一個位元組陣列,主要用來存放即將刷入out的內容,相當於一個臨時快取,在對輸出內容進行序列化時作為儲存目的地。 mu是一個mutex主要用來作執行緒安全的實習,當有多個goroutine同時往一個目的刷內容的時候,通過mutex保證每次寫入是一條完整的資訊。

2.std及整體結構

在前一篇文章中我們提到了log模組提供了一套包級別的簡單介面,使用該介面可以直接將日誌內容列印到標準錯誤。那麼該過程是怎麼實現的呢?其實就是通過一個內建的Logger型別的變數 "std" 來實現的。該變數使用:

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

進行初始化,預設輸出到系統的標準輸出 "os.Stderr" ,字首為空,使用日期加時間作為Log擡頭。

當我們呼叫 log.Print的時候是怎麼執行的呢?我們看其程式碼:

func Print(v ...interface{}) {
    std.Output(2, fmt.Sprint(v...))
}

這裡實際就是呼叫了Logger物件的 Output方法,將日誌內容按照fmt包中約定的格式轉義後傳給Output。Output定義如下 :

func (l *Logger) Output(calldepth int, s string) error

其中s為日誌沒有加字首和Log擡頭的具體內容,xxxxx 。該函式執行具體的將日誌刷入到對應的位置。

3.核心函式的實現

Logger.Output是執行具體的將日誌刷入到對應位置的方法。

該方法首先根據需要獲得當前時間和呼叫該方法的檔案及行號資訊。然後呼叫formatHeader方法將Log的字首和Log擡頭先格式化好 放入Logger.buf中,然後再將Log的內容存入到Logger.buf中,最後呼叫Logger.out.Write方法將完整的日誌寫入到輸出目的地中。

由於寫入檔案以及拼接buf的過程是執行緒非安全的,因此使用mutex保證每次寫入的原子性。

l.mu.Lock()
defer l.mu.Unlock()

將buf的拼接和檔案的寫入放入這個後面,使得在多個goroutine使用同一個Logger物件是,不會弄亂buf,也不會雜糅的寫入。

該方法的第一個引數最終會傳遞給runtime.Caller的skip,指的是跳過的棧的深度。這裡我記住給2就可以了。這樣就會得到我們呼叫log 是所處的位置。

在golang的註釋中說鎖住 runtime.Caller的過程比較重,這點我還是不很瞭解,只是從程式碼中看到其在這裡把鎖打開了。

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()
}

在formatHeader裡面首先將字首直接複製到Logger.buf中,然後根據flag選擇Log擡頭的內容,這裡用到了一個log模組實現的 itoa的方法,作用類似c的itoa,將一個整數轉換成一個字串。只是其轉換後將結果直接追加到了buf的尾部。

縱觀整個實現,最值得學習的就是執行緒安全的部分。在什麼位置合適做怎樣的同步操作。

4.對外介面的實現

在瞭解了核心格式化和輸出結構後,在看其封裝就非常簡單了,幾乎都是首先用Output進行日誌的記錄,然後在必要的時候 做os.exit或者panic的操作,這裡看下Fatal的實現。

func (l *Logger) Fatal(v ...interface{}) {
    l.Output(2, fmt.Sprint(v...))
    os.Exit(1)
}
// Fatalf is equivalent to l.Printf() followed by a call to os.Exit(1).
func (l *Logger) Fatalf(format string, v ...interface{}) {
    l.Output(2, fmt.Sprintf(format, v...))
    os.Exit(1)
}
// Fatalln is equivalent to l.Println() followed by a call to os.Exit(1).
func (l *Logger) Fatalln(v ...interface{}) {
    l.Output(2, fmt.Sprintln(v...))
    os.Exit(1)
}

這裡也驗證了我們之前做的Panic的結果,先做輸出日誌操作。再進行panic。

5.Golang的log模組設計

Golang的log模組主要提供了三類介面 :

  • Print : 一般的訊息輸出

  • Fatal : 類似assert一般的強行退出

  • Panic : 相當於OO裡面常用的異常捕獲

與其說log模組提供了三類日誌介面,不如說log模組僅僅是對類C中的 printf、assert、try...catch...的簡單封裝。Golang的log模組 並沒有對log進行分類、分級、過濾等其他類似log4j、log4c、zlog當中常見的概念。當然在使用中你可以通過新增prefix,來進行簡單的分級,或者改變Logger.out改變其輸出位置。但這些並沒有在API層面給出直觀的介面。

Golang的log模組就像是其目前僅專注於為伺服器程式設計一樣,他的log模組也專注於伺服器尤其是基礎元件而服務。就像nginx、redis、lighttpd、keepalived自己為自己寫了一個簡單的日誌模組而沒有實現log4c那樣龐大且複雜的日誌模組一樣。他的日誌模組僅僅需要為本服務按照需要的格式和方式提供介面將日誌輸出到目的地即可。

Golang的log模組可以進行一般的資訊記錄,assert時的資訊輸出,以及出現異常時的日誌記錄,通過對其Print的包裝可以實現更復雜的 輸出。因此這個log模組可謂是語言層面上非常基礎的一層庫,反應的是語言本身的特徵而不是一個服務應該怎樣怎樣。