Golang 標准庫log的實現


 
原創作品,允許轉載,轉載時請務必以超鏈接形式標明文章  原始出處 、作者信息和本聲明。否則將追究法律責任。 http://gotaly.blog.51cto.com/8861157/1406905

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

1.Logger結構

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

1
2
3
4
5
6
7
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" 來實現的。該變量使用:

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

進行初始化,默認輸出到系統的標准輸出 "os.Stderr" ,前綴為空,使用日期加時間作為Log抬頭。

當我們調用 log.Print的時候是怎么執行的呢?我們看其代碼:

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

這里實際就是調用了Logger對象的 Output方法,將日志內容按照fmt包中約定的格式轉義后傳給Output。Output定義如下 :

 

1
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保證每次寫入的原子性。

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

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

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

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

1
2
3
4
5
6
7
8
9
10
11
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的實現。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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模塊可謂是語言層面上非常基礎的一層庫,反應的是語言本身的特征而不是一個服務應該怎樣怎樣。



本文出自 “Done_in_72_hours” 博客,請務必保留此出處http://gotaly.blog.51cto.com/8861157/1406905


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM