01、介紹
Golang 語言的(de)标準庫中提供了(le)一個(gè)簡單的(de) log 日志包,它不僅提供了(le)很多(duō)函數,還(hái)定義了(le)一個(gè)包含很多(duō)方法的(de)類型 Logger。但是它也(yě)有缺點,比如不支持區(qū)分(fēn)日志級别,不支持日志文件切割等。
02、函數
Golang 的(de) log 包主要提供了(le)以下(xià)幾個(gè)具備輸出功能的(de)函數:
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{})
這(zhè)些函數的(de)使用(yòng)方法和(hé) fmt 包完全相同,通(tōng)過查看源碼可(kě)以發現,Fatal[ln|f] 和(hé) Panic[ln|f] 實際上是調用(yòng)的(de) Print[ln|f],而 Print[ln|f] 實際上是調用(yòng)的(de) Output() 函數。
其中 Fatal[ln|f] 是調用(yòng) Print[ln|f] 之後,又調用(yòng)了(le) os.Exit(1) 退出程序。
其中 Panic[ln|f] 是調用(yòng) Panic[ln|f] 之後,又調用(yòng)了(le) panic() 函數,抛出一個(gè)恐慌。
所以,我們很有必要閱讀一下(xià) Output() 函數的(de)源碼。
函數 Output() 的(de)源碼:
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
}
通(tōng)過閱讀 Output() 函數的(de)源碼,可(kě)以發現使用(yòng)互斥鎖來(lái)保證多(duō)個(gè) goroutine 寫日志的(de)安全,并且在調用(yòng) runtime.Caller() 函數之前,先釋放互斥鎖,獲取到信息後再加上互斥鎖來(lái)保證安全。
使用(yòng) formatHeader() 函數來(lái)格式化(huà)日志的(de)信息,然後保存到 buf 中,然後再把日志信息追加到 buf 的(de)末尾,然後再通(tōng)過判斷,查看日志是否爲空或末尾不是 \n,如果是就再把 \n 追加到 buf 的(de)末尾,最後将日志信息輸出。
函數 Output() 的(de)源碼也(yě)比較簡單,其中最值得(de)注意的(de)是 runtime.Caller() 函數,源碼如下(xià):
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
}
通(tōng)過閱讀 runtime.Caller() 函數的(de)源碼,可(kě)以發現它接收一個(gè) int 類型的(de)參數 skip,該參數表示跳過棧幀數,log 包中的(de)輸出功能的(de)函數,使用(yòng)的(de)默認值都是 2,原因是什(shén)麽?
舉例說明(míng),比如在 main 函數中調用(yòng) log.Print,方法調用(yòng)棧爲 main->log.Print->*Logger.Output->runtime.Caller,所以此時(shí)參數 skip 的(de)值爲 2,表示 main 函數中調用(yòng) log.Print 的(de)源文件和(hé)代碼行号;
參數值爲 1,表示 log.Print 函數中調用(yòng) *Logger.Output 的(de)源文件和(hé)代碼行号;參數值爲 0,表示 *Logger.Output 函數中調用(yòng) runtime.Caller 的(de)源文件和(hé)代碼行号。
至此,我們發現 log 包的(de)輸出功能的(de)函數,全部都是把信息輸出到控制台,那麽該怎麽将信息輸出到文件中呢(ne)?
函數 SetOutPut 就是用(yòng)來(lái)設置輸出目标的(de),源碼如下(xià):
func SetOutput(w io.Writer) {
std.mu.Lock()
defer std.mu.Unlock()
std.out = w
}
我們可(kě)以通(tōng)過函數 os.OpenFile 來(lái)打開一個(gè)用(yòng)于 I/O 的(de)文件,返回值作爲函數 SetOutput 的(de)參數。
除此之外,讀者應該還(hái)發現了(le)一個(gè)問題,輸出信息都是以日期和(hé)時(shí)間開頭,我們該怎麽記錄更加豐富的(de)信息呢(ne)?比如源文件和(hé)行号。
這(zhè)就用(yòng)到了(le)函數 SetFlags,它可(kě)以設置輸出的(de)格式,源碼如下(xià):
func SetFlags(flag int) {
std.SetFlags(flag)
}
參數 flag 的(de)值可(kě)以是以下(xià)任意常量:
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 和(hé) Lmicroseconds 分(fēn)别表示日期、時(shí)間和(hé)微秒,需要注意的(de)是,如果設置 Lmicroseconds,那麽設置 Ltime,也(yě)不會生效。
其中 Llongfile 和(hé) Lshortfile 分(fēn)别代碼絕對(duì)路徑、源文件名、行号,和(hé)代碼相對(duì)路徑、源文件名、行号,需要注意的(de)是,如果設置 Lshortfile,那麽即使設置 Llongfile,也(yě)不會生效。
其中 LUTC 表示設置時(shí)區(qū)爲 UTC 時(shí)區(qū)。
其中 LstdFlags 表示标準記錄器的(de)初始值,包含日期和(hé)時(shí)間。
截止到現在,還(hái)缺少點東西,就是日志信息的(de)前綴,比如我們需要區(qū)分(fēn)日志信息爲 DEBUG、INFO 和(hé) ERROR。是的(de),我們還(hái)有一個(gè)函數 SetPrefix 可(kě)以實現此功能,源碼如下(xià):
func SetPrefix(prefix string) {
std.SetPrefix(prefix)
}
函數 SetPrefix 接收一個(gè) string 類型的(de)參數,用(yòng)來(lái)設置日志信息的(de)前綴。
03、Logger
log 包定義了(le)一個(gè)包含很多(duō)方法的(de)類型 Logger。我們通(tōng)過查看輸出功能的(de)函數,發現它們都是調用(yòng) std.Output,std 是什(shén)麽?我們查看 log 包的(de)源碼。
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)
通(tōng)過閱讀源碼,我們發現 std 實際上是 Logger 類型的(de)一個(gè)實例,Output 是 Logger 的(de)一個(gè)方法。
std 通(tōng)過 New 函數創建,參數分(fēn)别是 os.Stderr、空字符串和(hé) LstdFlags,分(fēn)别表示标準錯誤輸出、空字符串前綴和(hé)日期時(shí)間。
Logger 類型的(de)字段,注釋已經說明(míng)了(le),這(zhè)裏就不再贅述了(le)。
自定義 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、總結
本文主要介紹 Golang 語言的(de)标準庫中的(de) log 包,包括 log 包的(de)函數和(hé)自定義類型 logger 的(de)使用(yòng)方法和(hé)一些細節上的(de)注意事項。開篇也(yě)提到了(le),log 包不支持日志文件的(de)切割,我們需要自己編碼去實現,或者使用(yòng)三方庫,比如 lumberjack。在生産環境中,一般比較少用(yòng) log 包來(lái)記錄日志,通(tōng)常會使用(yòng)三方庫來(lái)記錄日志,比如 zap 和(hé) logrus 等。