log包是go語言提供的一個簡單的日誌記錄功能,其中定義了一個結構體型別 Logger
,是整個包的基礎部分,包中的其他方法都是圍繞這整個結構體建立的.
Logger結構
Logger結構的定義如下:
type Logger struct {
mu sync.Mutex
prefix string
flag int
out io.Writer
buf []byte
}
- mu 是sync.Mutex,它是一個同步互斥鎖,用於保證日誌記錄的原子性.
- prefix 是輸入的日誌每一行的字首
- flag 是一個標誌,用於設定日誌的列印格式
- out 日誌的輸出目標,需要是一個實現了 io.Writer介面的物件,如: os.Stdout, os.Stderr, os.File等等
- buf 用於快取資料
與此同時還提供了一個構造方法用於建立 Logger:
func New(out io.Writer, prefix string, flag int) *Logger {
return &Logger{out: out, prefix: prefix, flag: flag}
}
還有圍繞Logger結構的幾個引數定義的方法:
func (l *Logger) SetOutput(w io.Writer) // 用於設定日誌輸出目標
func (l *Logger) SetPrefix(prefix string) // 用於設定每一行日誌的字首
func (l *Logger) Prefix() string // 獲取當前使用的字首
func (l *Logger) SetFlags(flag int) // 用於設定使用的輸出標誌
func (l *Logger) Flags() int // 獲取當前使用的標誌
這些方法都很簡單,只是給我們提供了一個可以修改和獲取當前日誌器的設定的方式.
flag可選值
在 log 包中,定義了一系列的常亮用於表示 flag,如下:
const (
Ldate = 1 << iota // 1 << 0 當地時區的日期: 2009/01/23
Ltime // 1 << 1 當地時區的時間: 01:23:23
Lmicroseconds // 1 << 2 顯示精度到微秒: 01:23:23.123123 (應該和Ltime一起使用)
Llongfile // 1 << 3 顯示完整檔案路徑和行號: /a/b/c/d.go:23
Lshortfile // 1 << 4 顯示當前檔名和行號: d.go:23 (如果與Llongfile一起出現,此項優先)
LUTC // 1 << 5如果設定了Ldata或者Ltime, 最好使用 UTC 時間而不是當地時區
LstdFlags = Ldate | Ltime // 標準日誌器的初始值
)
使用方法:
- 可以單獨使用某一個標誌,此時只會顯示對應的資訊
- 可以多個合併使用,只需要將多個標誌使用
|
連線即可
例如:
Ldate | Ltime // 2017/07/31 08:01:20
Ldate | Ltime | Lmicroseconds | Llongfile // 2017/07/31 08:01:20.123123 /a/b/c/d.go:23
常用方法
在 log 包中,定義了下面幾組方法:
func (l *Logger) Printf(format string, v ...interface{})
func (l *Logger) Print(v ...interface{})
func (l *Logger) Println(v ...interface{})
func (l *Logger) Fatal(v ...interface{})
func (l *Logger) Fatalf(format string, v ...interface{})
func (l *Logger) Fatalln(v ...interface{})
func (l *Logger) Panic(v ...interface{})
func (l *Logger) Panicf(format string, v ...interface{})
func (l *Logger) Panicln(v ...interface{})
即 Print*, Fatal*, Painc*, 這裡方法結尾的 f 或者 ln 就跟 fmt.Print 的含義是相同的,因此上面這九個方法的使用方式其實與 fmt.Print/f/ln
是一樣的.我們直接以沒有 f 或 ln 的方法為例來看看三組方法的程式碼:
func (l *Logger) Print(v ...interface{}) {
l.Output(2, fmt.Sprint(v...))
}
func (l *Logger) Fatal(v ...interface{}) {
l.Output(2, fmt.Sprint(v...))
os.Exit(1)
}
func (l *Logger) Panic(v ...interface{}) {
s := fmt.Sprint(v...)
l.Output(2, s)
panic(s)
}
可以看到其實三個方法 都呼叫了接收者(也就是Logger型別的例項或指標)的 Output 方法,這個方法後面在說,其實就是字面的意思,即用來輸出我們傳入進去的字串(fmt.Sprint方法將我們傳入的引數轉換為字串後返回)
不同的地方在於:
- Print 僅僅是輸出了資訊
- Fatal 不僅僅輸出了資訊,還使程式停止執行
- Painc 不僅僅輸出了資訊,還呼叫了 panic 丟擲錯誤
所以這三個方法的用處就顯而易見了.
Output方法
前面介紹了三組方法的內部都是呼叫了 Output 方法來實現的,也就是說實際的工作實在 Output 方法中執行的.
func (l *Logger) Output(calldepth int, s string) error {
now := time.Now()
var file string
var line int
l.mu.Lock()
defer l.mu.Unlock()
if l.flag&(Lshortfile|Llongfile) != 0 {
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
}
這裡需要提前說一下 runtime.Caller 函式,這個函式用於獲取呼叫Go程的棧上的函式呼叫所在的檔案和行號資訊。引數為 skip 表示我們需要獲取資訊的呼叫層級,返回值為 程式計數器(pc), 檔名,行號以及獲取成功與否的標誌。
在 Output 方法中,我們做了下面這些事情:
- 獲取當前事件
- 對 Logger例項進行加鎖操作
- 判斷Logger的標誌位是否包含 Lshortfile 或 Llongfile, 如果包含進入步驟4, 如果不包含進入步驟5
- 獲取當前函式呼叫所在的檔案和行號資訊
- 格式化資料,並將資料寫入到 l.out 中,完成輸出
- 解鎖操作
這裡我們注意到有一個 callpath 引數,這個引數是用於獲取某個指定層級的資訊,前面3組方法中,這裡使用的都是2, 這是因為,我們真正需要的檔名和行號是 呼叫 Print, Fatal, Panic 這些方法的地方,因此在呼叫 runtime.Caller
方法時,需要獲取棧中當前位置的前兩個位置處的資訊.
快捷方式
log 包除了提供了上述一些需要先建立 Logger 例項才能使用的方法之外,還給我們定義了一些快捷的方法,它的實現方式也很簡單,其實就是在 log包內預先定義了一個 Logger 例項叫 std:
var std = New(os.Stderr, "", LstdFlags)
然後定義了一些可以直接使用包來呼叫的方法:
func Output(calldepth int, s string) error
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{})
func SetFlags(flag int)
func Flags() int
func SetOutput(w io.Writer)
func SetPrefix(prefix string)
func Prefix() string
這些方法的內部實際上大部分都是直接呼叫了 std 的對應的方法來實現的,不過 Print*, Panic*, Fatal* 這些方法的內部還是呼叫了 std.Output 方法來實現的.
前面已經涵蓋了 log 包中的所有方法,除了下面兩個:
func itoa(buf *[]byte, i int, wid int)
func (l *Logger) formatHeader(buf *[]byte, t time.Time, file string, line int)
這裡就不細說了,主要就是用來完成資料的格式化操作的.