1. 代碼示例
這個示例程序展示如何使用最基本的 log 包。
// 這個示例程序展示如何使用最基本的log包
package main
import (
"log"
)
func init() {
log.SetPrefix("TRACE: ")
log.SetFlags(log.Ldate | log.Lmicroseconds | log.Llongfile)
}
func main() {
// Println寫到標準日誌記錄器
log.Println("message")
// Fatalln在調用Println()之後會接着調用os.Exit(1)
log.Fatalln("fatal message")
// Panicln在調用Println()之後會接着調用panic()
log.Panicln("panic message")
}
輸出:
TRACE: 2019/10/31 19:38:54.732475 /home/wohu/GoCode/src/hello.go:15: message
TRACE: 2019/10/31 19:38:54.732590 /home/wohu/GoCode/src/hello.go:18: fatal message
exit status 1
init
這個函數會在運行 main()
之前作爲程序初始化的一部分執行。通常程序會在這個 init()
函數裏配置日誌參數,這樣程序一開始就能使用 log 包進行正確的輸出。
2. 源碼說明
const (
// 將下面的位使用或運算符連接在一起,可以控制要輸出的信息。沒有
// 辦法控制這些信息出現的順序(下面會給出順序)或者打印的格式
// (格式在註釋裏描述)。這些項後面會有一個冒號:
// 2009/01/23 01:23:23.123123 /a/b/c/d.go:23: message
// 日期: 2009/01/23
Ldate = 1 << iota
// 時間: 01:23:23
Ltime
// 毫秒級時間: 01:23:23.123123。該設置會覆蓋Ltime標誌
Lmicroseconds
// 完整路徑的文件名和行號: /a/b/c/d.go:23
Llongfile
// 最終的文件名元素和行號: d.go:23
// 覆蓋 Llongfile
Lshortfile
// 標準日誌記錄器的初始值
LstdFlags = Ldate | Ltime
)
這些標誌用來控制可以寫到每個日誌項的其他信息。這些標誌被聲明爲常量。
// 日期: 2009/01/23
Ldate = 1 << iota
關鍵字 iota
在常量聲明區裏有特殊的作用。這個關鍵字讓編譯器爲每個常量複製相同的表達式,直到聲明區結束,或者遇到一個新的賦值語句。
關鍵字 iota
的另一個功能是, iota
的初始值爲 0,之後 iota
的值在每次處理爲常量後,都會自增 1。
const (
Ldate = 1 << iota // 1 << 0 = 000000001 = 1
Ltime // 1 << 1 = 000000010 = 2
Lmicroseconds // 1 << 2 = 000000100 = 4
Llongfile // 1 << 3 = 000001000 = 8
Lshortfile // 1 << 4 = 000010000 = 16
...
)
操作符 <<
對左邊的操作數執行按位左移操作。在每個常量聲明時,都將 1 按位左移 iota
個位置。最終的效果使爲每個常量賦予一個獨立位置的位,這正好是標誌希望的工作方式。
常量 LstdFlags
展示瞭如何使用這些標誌,
const (
...
LstdFlags = Ldate(1) | Ltime(2) = 00000011 = 3
)
因爲使用了複製操作符, LstdFlags
打破了 iota 常數鏈。由於有 |
運算符用於執行或操作,常量 LstdFlags
被賦值爲 3。
對位進行或操作等同於將每個位置的位組合在一起,作爲最終的值。如果對位 1 和 2 進行或操作,最終的結果就是 3。
3. 代碼分析
func init() {
...
log.SetFlags(log.Ldate | log.Lmicroseconds | log.Llongfile)
}
這裏我們將 Ldate
、 Lmicroseconds
和 Llongfile
標誌組合在一起,將該操作的值傳入 SetFlags
函數。這些標誌值組合在一起後,最終的值是 13,代表第 1、3 和 4 位爲 1(00001101)。
由於每個常量表示單獨一個位,這些標誌經過或操作組合後的值,可以表示每個需要的日誌參數。之後 log
包會按位檢查這個傳入的整數值,按照需求設置日誌項記錄的信息。
初始完 log
包後,可以看一下 main()
函數.
func main() {
// Println寫到標準日誌記錄器
log.Println("message")
// Fatalln在調用Println()之後會接着調用os.Exit(1)
log.Fatalln("fatal message")
// Panicln在調用Println()之後會接着調用panic()
log.Panicln("panic message")
}
上述代碼展示瞭如何使用 3 個函數 Println
、 Fatalln
和 Panicln
來寫日誌消息。這些函數也有可以格式化消息的版本,只需要用 f 替換結尾的 ln。
Fatal
系列函數用來寫日誌消息,然後使用 os.Exit(1)
終止程序。
Panic
系列函數用來寫日誌消息,然後觸發一個 panic
。
除非程序執行 recover
函數,否則會導致程序打印調用棧後終止。 Print
系列函數是寫日誌消息的標準方法。
log
包有一個很方便的地方就是,這些日誌記錄器是多 goroutine
安全的。這意味着在多個 goroutine
可以同時調用來自同一個日誌記錄器的這些函數,而不會有彼此間的寫衝突。
4. 定製日誌
要想創建一個定製的日誌記錄器,需要創建一個 Logger
類型值。可以給每個日誌記錄器配置一個單獨的目的地,並獨立設置其前綴和標誌。
讓我們來看一個示例程序,這個示例程序展示瞭如何創建不同的 Logger
類型的指針變量來支持不同的日誌等級。
// 這個示例程序展示如何創建定製的日誌記錄器
package main
import (
"io"
"io/ioutil"
"log"
"os"
)
var ( // 爲4個日誌等級聲明瞭4個Logger類型的指針變量
Trace *log.Logger // 記錄所有日誌
Info *log.Logger // 重要的信息
Warning *log.Logger // 需要注意的信息
Error *log.Logger // 非常嚴重的問題
)
func init() {
file, err := os.OpenFile("errors.txt",
os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)
if err != nil {
log.Fatalln("Failed to open error log file:", err)
}
Trace = log.New(ioutil.Discard, // 當某個等級的日誌不重要時,使用Discard變量可以禁用這個等級的日誌。
"TRACE: ",
log.Ldate|log.Ltime|log.Lshortfile)
Info = log.New(os.Stdout,
"INFO: ",
log.Ldate|log.Ltime|log.Lshortfile)
Warning = log.New(io.MultiWriter(file, os.Stdout),
"WARN: ",
log.Ldate|log.Ltime|log.Lshortfile)
// io.MultiWriter(file, os.Stderr)
/*
這個函數調用會返回一個io.Writer接口類型值,這個值包含之前打開的文件file,以及stderr。
MultiWriter函數是一個變參函數,可以接受任意個實現了io.Writer接口的值。
這個函數會返回一個io.Writer值,這個值會把所有傳入的io.Writer的值綁在一起。
當對這個返回值進行寫入時,會向所有綁在一起的io.Writer值做寫入。
這讓類似log.New這樣的函數可以同時向多個Writer做輸出。
現在,當我們使用Error記錄器記錄日誌時,輸出會同時寫到文件和stderr。
*/
Error = log.New(io.MultiWriter(file, os.Stderr),
"ERROR: ",
log.Ldate|log.Ltime|log.Lshortfile)
}
func main() {
Trace.Println("I have something standard to say")
Info.Println("Special Information")
Warning.Println("There is something you need to know about")
Error.Println("Something has failed")
}
使用了 log
包的 New
函數,它創建並正確初始化一個 Logger
類型的值。函數 New
會返回新創建的值的地址。在 New
函數創建對應值的時候,我們需要給它傳入一些參數,如下代碼所示:
// New創建一個新的Logger。out參數設置日誌數據將被寫入的目的地
// 參數prefix會在生成的每行日誌的最開始出現
// 參數flag定義日誌記錄包含哪些屬性
func New(out io.Writer, prefix string, flag int) *Logger {
return &Logger{out: out, prefix: prefix, flag: flag}
}
上述代碼來自 log
包的源代碼裏的 New
函數的聲明。第一個參數 out
指定了日誌要寫到的目的地。這個參數傳入的值必須實現了 io.Writer
接口。第二個參數 prefix
是之前看到的前綴,而日誌的標誌則是最後一個參數。
以上是我們通過 go
語言自帶的 log
來實現的自己的日誌工具。Go
社區很強大,社區的大佬們爲我們實現了更加強大好用的工具類,比如支持按照日期、大小滾動切割文件輸出;有着更細緻的日誌級別有更高的更好的性能;支持各種插件可以直接對接
elk、prometheus 等。下面爲大家介紹兩款日誌框架:
logrus : https://github.com/sirupsen/logrus
seelog:https://github.com/cihub/seelog
將上面代碼中的 main
函數註釋掉,修改文件名爲 mylog
,包名修改爲 package mylog
├── task.go
└── mylog
└── mylog.go
task.go
package main
import "mylog"
func main() {
mylog.Trace.Println("trace")
mylog.Info.Println("info")
mylog.Error.Println("error")
}
可以看到輸出爲:
INFO: 2020/04/20 18:24:25 task.go:7: info
ERROR: 2020/04/20 18:24:25 task.go:8: error