
無(wú)論是軟件開(kāi)發(fā)的調(diào)試階段還是軟件上線之后的運(yùn)行階段,日志一直都是非常重要的一個(gè)環(huán)節(jié),我們也應(yīng)該養(yǎng)成在程序中記錄日志的好習(xí)慣。
Go 語(yǔ)言內(nèi)置的log包實(shí)現(xiàn)了簡(jiǎn)單的日志服務(wù)。本文介紹了標(biāo)準(zhǔn)庫(kù)log的基本使用和第三日志庫(kù)的選型和使用。
1、原生Logger
log 包定義了 Logger 類(lèi)型,該類(lèi)型提供了一些格式化輸出的方法。本包也提供了一個(gè)預(yù)定義的 “標(biāo)準(zhǔn)”logger,可以通過(guò)調(diào)用函數(shù)Print系列(Print|Printf|Println)、Fatal系列(Fatal|Fatalf|Fatalln)、和Panic系列(Panic|Panicf|Panicln)來(lái)使用,比自行創(chuàng)建一個(gè) logger 對(duì)象更容易使用。
例如,我們可以像下面的代碼一樣直接通過(guò)log包來(lái)調(diào)用上面提到的方法,默認(rèn)它們會(huì)將日志信息打印到終端界面:
package main
import (
"log"
)
func main() {
log.Println("這是一條很普通的日志。")
v := "很普通的"
log.Printf("這是一條%s日志。\n", v)
log.Fatalln("這是一條會(huì)觸發(fā)fatal的日志。")
log.Panicln("這是一條會(huì)觸發(fā)panic的日志。")
}
編譯并執(zhí)行上面的代碼會(huì)得到如下輸出:
2017/06/19 14:04:17 這是一條很普通的日志。
2017/06/19 14:04:17 這是一條很普通的日志。
2017/06/19 14:04:17 這是一條會(huì)觸發(fā)fatal的日志。
logger 會(huì)打印每條日志信息的日期、時(shí)間,默認(rèn)輸出到系統(tǒng)的標(biāo)準(zhǔn)錯(cuò)誤。Fatal 系列函數(shù)會(huì)在寫(xiě)入日志信息后調(diào)用 os.Exit(1)。Panic 系列函數(shù)會(huì)在寫(xiě)入日志信息后 panic。
1.1 配置 logger配置
默認(rèn)情況下的 logger 只會(huì)提供日志的時(shí)間信息,但是很多情況下我們希望得到更多信息,比如記錄該日志的文件名和行號(hào)等。log標(biāo)準(zhǔn)庫(kù)中為我們提供了定制這些設(shè)置的方法。
log標(biāo)準(zhǔn)庫(kù)中的Flags函數(shù)會(huì)返回標(biāo)準(zhǔn) logger 的輸出配置,而SetFlags函數(shù)用來(lái)設(shè)置標(biāo)準(zhǔn) logger 的輸出配置。
func Flags() int
func SetFlags(flag int)
1.1.1 flag 選項(xiàng)
log標(biāo)準(zhǔn)庫(kù)提供了如下的 flag 選項(xiàng),它們是一系列定義好的常量。
const (
// 控制輸出日志信息的細(xì)節(jié),不能控制輸出的順序和格式。
// 輸出的日志在每一項(xiàng)后會(huì)有一個(gè)冒號(hào)分隔:例如2009/01/23 01:23:23.123123 /a/b/c/d.go:23: message
Ldate = 1 << iota // 日期:2009/01/23
Ltime // 時(shí)間:01:23:23
Lmicroseconds // 微秒級(jí)別的時(shí)間:01:23:23.123123(用于增強(qiáng)Ltime位)
Llongfile // 文件全路徑名+行號(hào): /a/b/c/d.go:23
Lshortfile // 文件名+行號(hào):d.go:23(會(huì)覆蓋掉Llongfile)
LUTC // 使用UTC時(shí)間
LstdFlags = Ldate | Ltime // 標(biāo)準(zhǔn)logger的初始值
)
下面我們?cè)谟涗浫罩局跋仍O(shè)置一下標(biāo)準(zhǔn) logger 的輸出選項(xiàng)如下:
func main() {
log.SetFlags(log.Llongfile | log.Lmicroseconds | log.Ldate)
log.Println("這是一條很普通的日志。")
}
編譯執(zhí)行后得到的輸出結(jié)果如下:
2017/06/19 14:05:17.494943 .../log_demo/main.go:11: 這是一條很普通的日志。
1.1.2 配置日志前綴
log標(biāo)準(zhǔn)庫(kù)中還提供了關(guān)于日志信息前綴的兩個(gè)方法:
func Prefix() string
func SetPrefix(prefix string)
其中Prefix函數(shù)用來(lái)查看標(biāo)準(zhǔn) logger 的輸出前綴,SetPrefix函數(shù)用來(lái)設(shè)置輸出前綴。
func main() {
log.SetFlags(log.Llongfile | log.Lmicroseconds | log.Ldate)
log.Println("這是一條很普通的日志。")
log.SetPrefix("[小王子]")
log.Println("這是一條很普通的日志。")
}
上面的代碼輸出如下:
[小王子]2017/06/19 14:05:57.940542 .../log_demo/main.go:13: 這是一條很普通的日志。
這樣我們就能夠在代碼中為我們的日志信息添加指定的前綴,方便之后對(duì)日志信息進(jìn)行檢索和處理。
1.1.3 配置日志輸出位置
func SetOutput(w io.Writer)
SetOutput函數(shù)用來(lái)設(shè)置標(biāo)準(zhǔn) logger 的輸出目的地,默認(rèn)是標(biāo)準(zhǔn)錯(cuò)誤輸出。
例如,下面的代碼會(huì)把日志輸出到同目錄下的xx.log文件中。
func main() {
logFile, err := os.OpenFile("./xx.log", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644)
if err != nil {
fmt.Println("open log file failed, err:", err)
return
}
log.SetOutput(logFile)
log.SetFlags(log.Llongfile | log.Lmicroseconds | log.Ldate)
log.Println("這是一條很普通的日志。")
log.SetPrefix("[小王子]")
log.Println("這是一條很普通的日志。")
}
如果你要使用標(biāo)準(zhǔn)的 logger,我們通常會(huì)把上面的配置操作寫(xiě)到init函數(shù)中。
func init() {
logFile, err := os.OpenFile("./xx.log", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644)
if err != nil {
fmt.Println("open log file failed, err:", err)
return
}
log.SetOutput(logFile)
log.SetFlags(log.Llongfile | log.Lmicroseconds | log.Ldate)
}
1.2 創(chuàng)建 logger
log標(biāo)準(zhǔn)庫(kù)中還提供了一個(gè)創(chuàng)建新 logger 對(duì)象的構(gòu)造函數(shù)–New,支持我們創(chuàng)建自己的 logger 示例。New函數(shù)的簽名如下:
func New(out io.Writer, prefix string, flag int) *Logger
New 創(chuàng)建一個(gè) Logger 對(duì)象。其中,參數(shù) out 設(shè)置日志信息寫(xiě)入的目的地。參數(shù) prefix 會(huì)添加到生成的每一條日志前面。參數(shù) flag 定義日志的屬性(時(shí)間、文件等等)。
舉個(gè)例子:
func main() {
logger := log.New(os.Stdout, "<New>", log.Lshortfile|log.Ldate|log.Ltime)
logger.Println("這是自定義的logger記錄的日志。")
}
將上面的代碼編譯執(zhí)行之后,得到結(jié)果如下:
<New>2017/06/19 14:06:51 main.go:34: 這是自定義的logger記錄的日志。
1.3 總結(jié)
Go 內(nèi)置的 log 庫(kù)功能有限,例如無(wú)法滿足記錄不同級(jí)別日志的情況,我們?cè)趯?shí)際的項(xiàng)目中根據(jù)自己的需要選擇使用第三方的日志庫(kù),如 logrus、zap 等。
2、第三方日志庫(kù)
2.1 日志選型需求整理
- 日志寫(xiě)入性能
- 日志級(jí)別分離,并且可分離成多個(gè)日志文件
- 可讀性與結(jié)構(gòu)化,Json格式或有分隔符,方便后續(xù)的日志采集、監(jiān)控等
- 能夠打印基本信息,如調(diào)用文件 / 函數(shù)名和行號(hào),日志時(shí)間等
- 日志書(shū)寫(xiě)友好,支持通過(guò)context自動(dòng)log trace等
- 文件切割,可按小時(shí)、天進(jìn)行日志拆分,或者按文件大小
- 文件定時(shí)刪除
- 開(kāi)源性,與其他開(kāi)源框架支持較好
- 多輸出 - 同時(shí)支持標(biāo)準(zhǔn)輸出,文件等
2.2 日志比對(duì)
2.2.1 功能比對(duì)
參考文檔:
搜看的許多日志框架,最后剩下兩款目前明顯性能比較好的Uber開(kāi)源的Zap和ZeroLog,參考github中開(kāi)源項(xiàng)目日志引用情況和日志周邊框架支持最終選用Zap。
| 需求點(diǎn) | go.uber.org/zap(國(guó)內(nèi)一些開(kāi)源項(xiàng)目見(jiàn)得比較多、性能也不錯(cuò)、推薦) | github.com/rs/zerolog |
|---|---|---|
| 日志寫(xiě)入性能 | 較高 | 高 |
| 日志級(jí)別分離 | 支持 | 支持 |
| 可讀性 (Json格式或有分隔符,方便后續(xù)的日志采集、監(jiān)控等) | json格式 | json格式 |
| 易用性:接入方便,書(shū)寫(xiě)方便(格式化),可Hook注入trace_id等 | 自動(dòng)接入時(shí)間、代碼信息、日志級(jí)別,支持Hook | 可支持接入時(shí)間、代碼信息、日志級(jí)別等,支持Hook |
| 文件切割 (可按時(shí)間、文件大小日志拆分) | 不支持,可通過(guò)lumberjack實(shí)現(xiàn) | 支持 |
| 定時(shí)刪除 | 支持 | 支持 |
| 多輸出 - 同時(shí)支持標(biāo)準(zhǔn)輸出,文件等 | 支持 | 支持 |
2.2.3 性能數(shù)據(jù)比對(duì)
根據(jù)Uber-go Zap的文檔,它的性能比類(lèi)似的結(jié)構(gòu)化日志包更好——也比標(biāo)準(zhǔn)庫(kù)更快。 以下是Zap發(fā)布的基準(zhǔn)測(cè)試信息

記錄一個(gè)靜態(tài)字符串,沒(méi)有任何上下文或printf風(fēng)格的模板:

下一篇我們會(huì)來(lái)講講高性能日志框架Zap的使用,以及如何滿足我們對(duì)于日志框架豐富的使用需求,我們下期見(jiàn),Peace??
我是簡(jiǎn)凡,一個(gè)勵(lì)志用最簡(jiǎn)單的語(yǔ)言,描述最復(fù)雜問(wèn)題的新時(shí)代農(nóng)民工。求點(diǎn)贊,求關(guān)注,如果你對(duì)此篇文章有什么疑惑,歡迎在我的微信公眾號(hào)中留言,我還可以為你提供以下幫助:
- 幫助建立自己的知識(shí)體系
- 互聯(lián)網(wǎng)真實(shí)高并發(fā)場(chǎng)景實(shí)戰(zhàn)講解
- 不定期分享Golang、Java相關(guān)業(yè)內(nèi)的經(jīng)典場(chǎng)景實(shí)踐
我的博客:https://besthpt.github.io/
我的微信:bestbear666
微信公眾號(hào):"簡(jiǎn)凡丶"