Go 學習筆記(31)— Go 語言內置包 log

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

這裏我們將 LdateLmicrosecondsLlongfile 標誌組合在一起,將該操作的值傳入 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 個函數 PrintlnFatallnPanicln 來寫日誌消息。這些函數也有可以格式化消息的版本,只需要用 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
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章