Golang 語(yǔ)言的標(biāo)準(zhǔn)庫(kù) log 包怎么使用?
01、介紹
Golang 語(yǔ)言的標(biāo)準(zhǔn)庫(kù)中提供了一個(gè)簡(jiǎn)單的 log 日志包,它不僅提供了很多函數(shù),還定義了一個(gè)包含很多方法的類型 Logger。但是它也有缺點(diǎn),比如不支持區(qū)分日志級(jí)別,不支持日志文件切割等。
02、函數(shù)
Golang 的 log 包主要提供了以下幾個(gè)具備輸出功能的函數(shù):
- func Fatal(v ...interface{})
- func Fatalf(format string, v ...interface{})
- func Fatalln(v ...interface{})
- func Panic(v ...interface{})
- func Panicf(format string, v ...interface{})
- func Panicln(v ...interface{})
- func Print(v ...interface{})
- func Printf(format string, v ...interface{})
- func Println(v ...interface{})
這些函數(shù)的使用方法和 fmt 包完全相同,通過(guò)查看源碼可以發(fā)現(xiàn),F(xiàn)atal[ln|f] 和 Panic[ln|f] 實(shí)際上是調(diào)用的 Print[ln|f],而 Print[ln|f] 實(shí)際上是調(diào)用的 Output() 函數(shù)。
其中 Fatal[ln|f] 是調(diào)用 Print[ln|f] 之后,又調(diào)用了 os.Exit(1) 退出程序。
其中 Panic[ln|f] 是調(diào)用 Panic[ln|f] 之后,又調(diào)用了 panic() 函數(shù),拋出一個(gè)恐慌。
所以,我們很有必要閱讀一下 Output() 函數(shù)的源碼。
函數(shù) Output() 的源碼:
- func (l *Logger) Output(calldepth int, s string) error {
- now := time.Now() // get this early.
- var file string
- var line int
- l.mu.Lock()
- defer l.mu.Unlock()
- 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()
- }
- 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')
- }
- _, err := l.out.Write(l.buf)
- return err
- }
通過(guò)閱讀 Output() 函數(shù)的源碼,可以發(fā)現(xiàn)使用互斥鎖來(lái)保證多個(gè) goroutine 寫(xiě)日志的安全,并且在調(diào)用 runtime.Caller() 函數(shù)之前,先釋放互斥鎖,獲取到信息后再加上互斥鎖來(lái)保證安全。
使用 formatHeader() 函數(shù)來(lái)格式化日志的信息,然后保存到 buf 中,然后再把日志信息追加到 buf 的末尾,然后再通過(guò)判斷,查看日志是否為空或末尾不是 \n,如果是就再把 \n 追加到 buf 的末尾,最后將日志信息輸出。
函數(shù) Output() 的源碼也比較簡(jiǎn)單,其中最值得注意的是 runtime.Caller() 函數(shù),源碼如下:
- func Caller(skip int) (pc uintptr, file string, line int, ok bool) {
- rpc := make([]uintptr, 1)
- n := callers(skip+1, rpc[:])
- if n < 1 {
- return
- }
- frame, _ := CallersFrames(rpc).Next()
- return frame.PC, frame.File, frame.Line, frame.PC != 0
- }
通過(guò)閱讀 runtime.Caller() 函數(shù)的源碼,可以發(fā)現(xiàn)它接收一個(gè) int 類型的參數(shù) skip,該參數(shù)表示跳過(guò)棧幀數(shù),log 包中的輸出功能的函數(shù),使用的默認(rèn)值都是 2,原因是什么?
舉例說(shuō)明,比如在 main 函數(shù)中調(diào)用 log.Print,方法調(diào)用棧為 main->log.Print->*Logger.Output->runtime.Caller,所以此時(shí)參數(shù) skip 的值為 2,表示 main 函數(shù)中調(diào)用 log.Print 的源文件和代碼行號(hào);
參數(shù)值為 1,表示 log.Print 函數(shù)中調(diào)用 *Logger.Output 的源文件和代碼行號(hào);參數(shù)值為 0,表示 *Logger.Output 函數(shù)中調(diào)用 runtime.Caller 的源文件和代碼行號(hào)。
至此,我們發(fā)現(xiàn) log 包的輸出功能的函數(shù),全部都是把信息輸出到控制臺(tái),那么該怎么將信息輸出到文件中呢?
函數(shù) SetOutPut 就是用來(lái)設(shè)置輸出目標(biāo)的,源碼如下:
- func SetOutput(w io.Writer) {
- std.mu.Lock()
- defer std.mu.Unlock()
- std.out = w
- }
我們可以通過(guò)函數(shù) os.OpenFile 來(lái)打開(kāi)一個(gè)用于 I/O 的文件,返回值作為函數(shù) SetOutput 的參數(shù)。
除此之外,讀者應(yīng)該還發(fā)現(xiàn)了一個(gè)問(wèn)題,輸出信息都是以日期和時(shí)間開(kāi)頭,我們?cè)撛趺从涗浉迂S富的信息呢?比如源文件和行號(hào)。
這就用到了函數(shù) SetFlags,它可以設(shè)置輸出的格式,源碼如下:
- func SetFlags(flag int) {
- std.SetFlags(flag)
- }
參數(shù) flag 的值可以是以下任意常量:
- const (
- Ldate = 1 << iota // the date in the local time zone: 2009/01/23
- Ltime // the time in the local time zone: 01:23:23
- Lmicroseconds // microsecond resolution: 01:23:23.123123. assumes Ltime.
- Llongfile // full file name and line number: /a/b/c/d.go:23
- Lshortfile // final file name element and line number: d.go:23. overrides Llongfile
- LUTC // if Ldate or Ltime is set, use UTC rather than the local time zone
- Lmsgprefix // move the "prefix" from the beginning of the line to before the message
- LstdFlags = Ldate | Ltime // initial values for the standard logger
- )
其中 Ldate、Ltime 和 Lmicroseconds 分別表示日期、時(shí)間和微秒,需要注意的是,如果設(shè)置 Lmicroseconds,那么設(shè)置 Ltime,也不會(huì)生效。
其中 Llongfile 和 Lshortfile 分別代碼絕對(duì)路徑、源文件名、行號(hào),和代碼相對(duì)路徑、源文件名、行號(hào),需要注意的是,如果設(shè)置 Lshortfile,那么即使設(shè)置 Llongfile,也不會(huì)生效。
其中 LUTC 表示設(shè)置時(shí)區(qū)為 UTC 時(shí)區(qū)。
其中 LstdFlags 表示標(biāo)準(zhǔn)記錄器的初始值,包含日期和時(shí)間。
截止到現(xiàn)在,還缺少點(diǎn)東西,就是日志信息的前綴,比如我們需要區(qū)分日志信息為 DEBUG、INFO 和 ERROR。是的,我們還有一個(gè)函數(shù) SetPrefix 可以實(shí)現(xiàn)此功能,源碼如下:
- func SetPrefix(prefix string) {
- std.SetPrefix(prefix)
- }
函數(shù) SetPrefix 接收一個(gè) string 類型的參數(shù),用來(lái)設(shè)置日志信息的前綴。
03、Logger
log 包定義了一個(gè)包含很多方法的類型 Logger。我們通過(guò)查看輸出功能的函數(shù),發(fā)現(xiàn)它們都是調(diào)用 std.Output,std 是什么?我們查看 log 包的源碼。
- type Logger struct {
- mu sync.Mutex // ensures atomic writes; protects the following fields
- prefix string // prefix on each line to identify the logger (but see Lmsgprefix)
- flag int // properties
- out io.Writer // destination for output
- buf []byte // for accumulating text to write
- }
- func New(out io.Writer, prefix string, flag int) *Logger {
- return &Logger{out: out, prefix: prefix, flag: flag}
- }
- var std = New(os.Stderr, "", LstdFlags)
通過(guò)閱讀源碼,我們發(fā)現(xiàn) std 實(shí)際上是 Logger 類型的一個(gè)實(shí)例,Output 是 Logger 的一個(gè)方法。
std 通過(guò) New 函數(shù)創(chuàng)建,參數(shù)分別是 os.Stderr、空字符串和 LstdFlags,分別表示標(biāo)準(zhǔn)錯(cuò)誤輸出、空字符串前綴和日期時(shí)間。
Logger 類型的字段,注釋已經(jīng)說(shuō)明了,這里就不再贅述了。
自定義 Logger:
- func main () {
- logFile, err := os.OpenFile("error1.log", os.O_RDWR|os.O_CREATE|os.O_APPEND, 0755)
- if err != nil {
- fmt.Println(err)
- return
- }
- defer logFile.Close()
- logs := DefinesLogger(logFile, "", log.LstdFlags|log.Lshortfile)
- logs.Debug("message")
- logs.Debugf("%s", "content")
- }
- // 自定義 logger
- type Logger struct {
- definesLogger *log.Logger
- }
- type Level int8
- const(
- LevelDebug Level = iota
- LevelInfo
- LevelError
- )
- func (l Level) String() string {
- switch l {
- case LevelDebug:
- return " [debug] "
- case LevelInfo:
- return " [info] "
- case LevelError:
- return " [error] "
- }
- return ""
- }
- func DefinesLogger(w io.Writer, prefix string, flag int) *Logger {
- l := log.New(w, prefix, flag)
- return &Logger{definesLogger: l}
- }
- func (l *Logger) Debug(v ...interface{}) {
- l.definesLogger.Print(LevelDebug, fmt.Sprint(v...))
- }
- func (l *Logger) Debugf(format string, v ...interface{}) {
- l.definesLogger.Print(LevelDebug, fmt.Sprintf(format, v...))
- }
- func (l *Logger) Info(v ...interface{}) {
- l.definesLogger.Print(LevelInfo, fmt.Sprint(v...))
- }
- func (l *Logger) Infof(format string, v ...interface{}) {
- l.definesLogger.Print(LevelInfo, fmt.Sprintf(format, v...))
- }
- func (l *Logger) Error(v ...interface{}) {
- l.definesLogger.Print(LevelError, fmt.Sprint(v...))
- }
- func (l *Logger) Errorf(format string, v ...interface{}) {
- l.definesLogger.Print(LevelError, fmt.Sprintf(format, v...))
- }
04、總結(jié)
本文主要介紹 Golang 語(yǔ)言的標(biāo)準(zhǔn)庫(kù)中的 log 包,包括 log 包的函數(shù)和自定義類型 logger 的使用方法和一些細(xì)節(jié)上的注意事項(xiàng)。開(kāi)篇也提到了,log 包不支持日志文件的切割,我們需要自己編碼去實(shí)現(xiàn),或者使用三方庫(kù),比如 lumberjack。在生產(chǎn)環(huán)境中,一般比較少用 log 包來(lái)記錄日志,通常會(huì)使用三方庫(kù)來(lái)記錄日志,比如 zap 和 logrus 等。