在我們開發程式後,如果有一些問題需要對程式進行調試的時候,日誌是必不可少的,這是我們剖析器問題常用的手段。
日誌使用
日誌分析,就是根據輸出的日誌資訊,分析挖掘可能的問題,我們使用fmt.Println
系列函數也可以達到目的,因為它們也可以把我們需要的資訊輸出到終端或者其他檔案中。不過fmt.Println
系列函數輸出的系統比較簡單,比如沒有時間,也沒有原始碼的行數等,對於我們排查問題,缺少了很多資訊。
對此,Go語言為我們提供了標準的log
包,來追蹤記錄檔的記錄。下面我們看看日誌包log
的使用。
func main() { log.Println("飛雪無情的部落格:","http://www.flysnow.org") log.Printf("飛雪無情的公眾號:%s\n","flysnow_org")}
使用非常簡單,函數名字和用法也和fmt
包很相似,但是它的輸出預設帶了時間戳記。
2017/04/29 13:18:44 飛雪無情的部落格: http://www.flysnow.org2017/04/29 13:18:44 飛雪無情的公眾號:flysnow_org
這樣我們很清晰的就知道了,記錄這些日誌的時間,這對我們排查問題,非常有用。
有了時間了,我們還想要更多的資訊,必然發生的原始碼行號等,對此日誌包log
為我們提供了可定製化的配製,讓我們可以自己定製日誌的抬頭資訊。
func init(){ log.SetFlags(log.Ldate|log.Lshortfile)}
我們使用init
函數,這個函數在main
函數執行之前就可以初始化,可以幫我們做一些配置,這裡我們自訂日誌的抬頭資訊為時間+檔案名稱+原始碼所在行號。也就是log.Ldate|log.Lshortfile
,中間是一個位元運算符|
,然後通過函數log.SetFlags
進行設定。現在我們再運行下看看輸出的日誌。
2017/04/29 main.go:10: 飛雪無情的部落格: http://www.flysnow.org2017/04/29 main.go:11: 飛雪無情的公眾號:flysnow_org
比著上一個例子,多了源檔案以及行號,但是少了時間,這就是我們自訂出來的結果。現在我們看看log
包為我們提供了那些可以定義的選項常量。
const ( Ldate = 1 << iota //日期樣本: 2009/01/23 Ltime //時間樣本: 01:23:23 Lmicroseconds //毫秒樣本: 01:23:23.123123. Llongfile //絕對路徑和行號: /a/b/c/d.go:23 Lshortfile //檔案和行號: d.go:23. LUTC //日期時間轉為0時區的 LstdFlags = Ldate | Ltime //Go提供的標準抬頭資訊)
這是log包定義的一些抬頭資訊,有日期、時間、毫秒時間、絕對路徑和行號、檔案名稱和行號等,在上面都有注釋說明,這裡需要注意的是:如果設定了Lmicroseconds
,那麼Ltime
就不生效了;設定了Lshortfile
, Llongfile
也不會生效,大家自己可以測試一下。
LUTC
比較特殊,如果我們配置了時間標籤,那麼如果設定了LUTC
的話,就會把輸出的日期時間轉為0時區的日期時間顯示。
log.SetFlags(log.Ldate|log.Ltime |log.LUTC)
那麼對我們東八區的時間來說,就會減去8個小時,我們看輸出:
2017/04/29 05:46:29 飛雪無情的部落格: http://www.flysnow.org2017/04/29 05:46:29 飛雪無情的公眾號:flysnow_org
最後一個LstdFlags
表示標準的日誌抬頭資訊,也就是預設的,包含日期和具體時間。
我們大部分情況下,都有很多業務,每個業務都需要記錄日誌,那麼有沒有辦法,能區分這些業務呢?這樣我們在尋找日誌的時候,就方便多了。
對於這種情況,Go語言也幫我們考慮到了,這就是設定日誌的首碼,比如一個使用者中心系統的日誌,我們可以這麼設定。
func init(){ log.SetPrefix("【UserCenter】") log.SetFlags(log.LstdFlags | log.Lshortfile |log.LUTC)}
通過log.SetPrefix
可以指定輸出日誌的首碼,這裡我們指定為【UserCenter】
,然後就可以看到日誌的列印輸出已經清晰的標記出我們的這些日誌是屬於哪些業務的啦。
【UserCenter】2017/04/29 05:53:26 main.go:11: 飛雪無情的部落格: http://www.flysnow.org【UserCenter】2017/04/29 05:53:26 main.go:12: 飛雪無情的公眾號:flysnow_org
log
包除了有Print
系列的函數,還有Fatal
以及Panic
系列的函數,其中Fatal
表示程式遇到了致命的錯誤,需要退出,這時候使用Fatal
記錄日誌後,然後程式退出,也就是說Fatal
相當於先調用Print
列印日誌,然後再調用os.Exit(1)
退出程式。
同理Panic
系列的函數也一樣,表示先使用Print
記錄日誌,然後調用panic()
函數拋出一個恐慌,這時候除非使用recover()
函數,否則程式就會列印錯誤堆棧資訊,然後程式終止。
這裡貼下這幾個系列函數的原始碼,更好理解。
func Println(v ...interface{}) { std.Output(2, fmt.Sprintln(v...))}func Fatalln(v ...interface{}) { std.Output(2, fmt.Sprintln(v...)) os.Exit(1)}func Panicln(v ...interface{}) { s := fmt.Sprintln(v...) std.Output(2, s) panic(s)}
實現原理
通過上面的原始碼,我們發現,日誌包log
的這些函數都是類似的,關鍵的輸出日誌就在於std.Output
方法。
func New(out io.Writer, prefix string, flag int) *Logger { return &Logger{out: out, prefix: prefix, flag: flag}}var std = New(os.Stderr, "", LstdFlags)
從以上原始碼可以看出,變數std
其實是一個*Logger
,通過log.New
函數建立,預設輸出到os.Stderr
裝置,首碼為空白,日誌抬頭資訊為標準抬頭LstdFlags
。
os.Stderr
對應的是UNIX裡的標準錯誤警告資訊的輸出裝置,同時被作為預設的日誌輸出目的地。初次之外,還有標準輸出裝置os.Stdout
以及標準輸入裝置os.Stdin
。
var ( Stdin = NewFile(uintptr(syscall.Stdin), "/dev/stdin") Stdout = NewFile(uintptr(syscall.Stdout), "/dev/stdout") Stderr = NewFile(uintptr(syscall.Stderr), "/dev/stderr"))
以上就是定義的UNIX的標準的三種裝置,分別用於輸入、輸出和警告錯誤資訊。理解了os.Stderr
,現在我們看下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}
- 欄位
mu
是一個互斥鎖,主要是是保證這個日誌記錄器Logger
在多goroutine下也是安全的。
- 欄位
prefix
是每一行日誌的首碼
- 欄位
flag
是日誌抬頭資訊
- 欄位
out
是日誌輸出的目的地,預設情況下是os.Stderr
。
- 欄位
buf
是一次日誌輸出文本緩衝,最終會被寫到out
裡。
瞭解了結構體Logger
的欄位,現在就可以看下它最重要的方法Output
了,這個方法會輸出格式化好的日誌資訊。
func (l *Logger) Output(calldepth int, s string) error { now := time.Now() // get this early. var file string var line int //加鎖,保證多goroutine下的安全 l.mu.Lock() defer l.mu.Unlock() //如果配置了擷取檔案和行號的話 if l.flag&(Lshortfile|Llongfile) != 0 { //因為runtime.Caller代價比較大,先不加鎖 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') } //輸出拼接好的緩衝buf裡的日誌資訊到目的地 _, err := l.out.Write(l.buf) return err}
整個代碼比較簡潔,為了多goroutine安全互斥鎖也用上了,但是在擷取呼叫堆疊資訊的時候,又要先解鎖,因為這個過程比較重。擷取到檔案、行號等資訊後,繼續加互斥鎖保證安全。
後面的就比較簡單了,formatHeader
方法主要是格式化日誌抬頭資訊,然後儲存在buf
這個緩衝中,最後再把我們自己的日誌資訊拼接到緩衝buf
的後面,然後為一次log日誌輸出追加一個分行符號,這樣每次日誌輸出都是一行一行的。
有了最終的日誌資訊buf
,然後把它寫到輸出的目的地out
裡就可以了,這是一個實現了io.Writer
介面的類型,只要實現了這個介面,都可以當作輸出目的地。
func (l *Logger) SetOutput(w io.Writer) { l.mu.Lock() defer l.mu.Unlock() l.out = w}
log
包的SetOutput
函數,可以設定輸出目的地。這裡稍微簡單介紹下runtime.Caller
,它可以擷取運行時方法的調用資訊。
func Caller(skip int) (pc uintptr, file string, line int, ok bool)
參數skip
表示跳過棧幀數,0
表示不跳過,也就是runtime.Caller
的調用者。1
的話就是再向上一層,表示調用者的調用者。
log日誌包裡使用的是2
,也就是表示我們在原始碼中調用log.Print
、log.Fatal
和log.Panic
這些函數的調用者。
以main
函數調用log.Println
為例,是main->log.Println->*Logger.Output->runtime.Caller
這麼一個方法調用棧,所以這時候,skip的值分別代表:
0
表示*Logger.Output
中調用runtime.Caller
的原始碼檔案和行號
1
表示log.Println
中調用*Logger.Output
的原始碼檔案和行號
2
表示main
中調用log.Println
的原始碼檔案和行號
所以這也是log
包裡的這個skip
的值為什麼一直是2
的原因。
定製自己的日誌
通過上面的源碼分析,我們知道日誌記錄的根本就在於一個日誌記錄器Logger
,所以我們定製自己的日誌,其實就是建立不同的Logger
。
var ( Info *log.Logger Warning *log.Logger Error * log.Logger)func init(){ errFile,err:=os.OpenFile("errors.log",os.O_CREATE|os.O_WRONLY|os.O_APPEND,0666) if err!=nil{ log.Fatalln("開啟記錄檔失敗:",err) } Info = log.New(os.Stdout,"Info:",log.Ldate | log.Ltime | log.Lshortfile) Warning = log.New(os.Stdout,"Warning:",log.Ldate | log.Ltime | log.Lshortfile) Error = log.New(io.MultiWriter(os.Stderr,errFile),"Error:",log.Ldate | log.Ltime | log.Lshortfile)}func main() { Info.Println("飛雪無情的部落格:","http://www.flysnow.org") Warning.Printf("飛雪無情的公眾號:%s\n","flysnow_org") Error.Println("歡迎關注留言")}
我們根據記錄層級定義了三種不同的Logger,分別為Info
,Warning
,Error
,用於不同層級日誌的輸出。這三種日誌記錄器都是使用log.New
函數進行建立。
這裡建立Logger的時候,Info
和Warning
都比較正常,Error
這裡採用了多個目的地輸出,這裡可以同時把錯誤記錄檔輸出到os.Stderr
以及我們建立的errors.log
檔案中。
io.MultiWriter
函數可以封裝多個io.Writer
為一個io.Writer
,這樣我們就可以達到同時對多個io.Writer
輸出日誌的目的。
io.MultiWriter
的實現也很簡單,定義一個類型實現io.Writer
,然後在實現的Write
方法裡迴圈調用要封裝的多個Writer
介面的Write
方法即可。
func (t *multiWriter) Write(p []byte) (n int, err error) { for _, w := range t.writers { n, err = w.Write(p) if err != nil { return } if n != len(p) { err = ErrShortWrite return } } return len(p), nil}
這裡我們通過定義了多個Logger來區分不同的記錄層級,使用比較麻煩,針對這種情況,可以使用第三方的log架構,也可以自定封裝定義,直接通過不同層級的方法來記錄不同層級的日誌,還可以設定記錄日誌的層級等。
每個golang包只需要一篇最好文章 系列
http://www.nextblockchain.top/books/golangpackage/summary