深入探究Golang中l(wèi)og標準庫的使用
Go 語言標準庫中的 log 包設(shè)計簡潔明了,易于上手,可以輕松記錄程序運行時的信息、調(diào)試錯誤以及跟蹤代碼執(zhí)行過程中的問題等。使用 log 包無需繁瑣的配置即可直接使用。本文旨在深入探究 log 包的使用和原理,幫助讀者更好地了解和掌握它。
使用
先來看一個 log 包的使用示例:
package main
import "log"
func main() {
log.Print("Print")
log.Printf("Printf: %s", "print")
log.Println("Println")
log.Fatal("Fatal")
log.Fatalf("Fatalf: %s", "fatal")
log.Fatalln("Fatalln")
log.Panic("Panic")
log.Panicf("Panicf: %s", "panic")
log.Panicln("Panicln")
}假設(shè)以上代碼存放在 main.go 中,通過 go run main.go 執(zhí)行代碼將會得到如下輸出:
$ go run main.go
2023/03/08 22:33:22 Print
2023/03/08 22:33:22 Printf: print
2023/03/08 22:33:22 Println
2023/03/08 22:33:22 Fatal
exit status 1
以上示例代碼中使用 log 包提供的 9 個函數(shù)分別對日志進行輸出,最終得到 4 條打印日志。我們來分析下每個日志函數(shù)的作用,來看看為什么出現(xiàn)這樣的結(jié)果。
log 包提供了 3 類共計 9 種方法來輸出日志內(nèi)容。
| 函數(shù)名 | 作用 | 使用示例 |
|---|---|---|
| 打印日志 | log.Print("Print") | |
| Printf | 打印格式化日志 | log.Printf("Printf: %s", "print") |
| Println | 打印日志并換行 | log.Println("Println") |
| Panic | 打印日志后執(zhí)行 panic(s)(s 為日志內(nèi)容) | log.Panic("Panic") |
| Panicf | 打印格式化日志后執(zhí)行 panic(s) | log.Panicf("Panicf: %s", "panic") |
| Panicln | 打印日志并換行后執(zhí)行 panic(s) | log.Panicln("Panicln") |
| Fatal | 打印日志后執(zhí)行 os.Exit(1) | log.Fatal("Fatal") |
| Fatalf | 打印格式化日志后執(zhí)行 os.Exit(1) | log.Fatalf("Fatalf: %s", "fatal") |
| Fatalln | 打印日志并換行后執(zhí)行 os.Exit(1) | log.Panicln("Panicln") |
根據(jù)以上表格說明,我們可以知道,log 包在執(zhí)行 log.Fatal("Fatal") 時,程序打印完日志就通過 os.Exit(1) 退出了。這也就可以解釋上面的示例程序,為什么打印了 9 次日志,卻只輸出了 4 條日志,并且最后程序退出碼為 1 了。
以上是 log 包最基本的使用方式,如果我們想對日志輸出做一些定制,可以使用 log.New 創(chuàng)建一個自定義 logger:
logger := log.New(os.Stdout, "[Debug] - ", log.Lshortfile)
log.New 函數(shù)接收三個參數(shù),分別用來指定:日志輸出位置(一個 io.Writer 對象)、日志前綴(字符串,每次打印日志都會跟隨輸出)、日志屬性(定義好的常量,稍后會詳細講解)。
使用示例:
package main
import (
"log"
"os"
)
func main() {
logger := log.New(os.Stdout, "[Debug] - ", log.Lshortfile)
logger.Println("custom logger")
}示例輸出:
[Debug] - main.go:10: custom logger
以上示例中,指定日志輸出到 os.Stdout,即標準輸出;日志前綴 [Debug] - 會自動被加入到每行日志的行首;這條日志沒有打印當前時間,而是打印了文件名和行號,這是 log.Lshortfile 日志屬性的作用。
日志屬性可選項如下:
| 屬性 | 說明 |
|---|---|
| Ldate | 當前時區(qū)的日期,格式:2009/01/23 |
| Ltime | 當前時區(qū)的時間,格式:01:23:23 |
| Lmicroseconds | 當前時區(qū)的時間,格式:01:23:23.123123,精確到微妙 |
| Llongfile | 全文件名和行號,格式:/a/b/c/d.go:23 |
| Lshortfile | 當前文件名和行號,格式:d.go:23,會覆蓋 Llongfile |
| LUTC | 使用 UTC 而非本地時區(qū),推薦日志全部使用 UTC 時間 |
| Lmsgprefix | 將 prefix 內(nèi)容從行首移動到日志內(nèi)容前面 |
| LstdFlags | 標準 logger 對象的初始值(等于:Ldate|Ltime) |
這些屬性都是預定義好的常量,不能修改,可以通過 | 運算符組合使用(如:log.Ldate|log.Ltime|log.Lshortfile)。
使用 log.New 函數(shù)創(chuàng)建 logger 對象以后,依然可以通過 logger 對象的方法修改其屬性值:
| 方法 | 作用 |
|---|---|
| SetOutput | 設(shè)置日志輸出位置 |
| SetPrefix | 設(shè)置日志輸出前綴 |
| SetFlags | 設(shè)置日志屬性 |
現(xiàn)在我們來看一個更加完整的使用示例:
package main
import (
"log"
"os"
)
func main() {
// 準備日志文件
logFile, _ := os.Create("demo.log")
defer func() { _ = logFile.Close() }()
// 初始化日志對象
logger := log.New(logFile, "[Debug] - ", log.Lshortfile|log.Lmsgprefix)
logger.Print("Print")
logger.Println("Println")
// 修改日志配置
logger.SetOutput(os.Stdout)
logger.SetPrefix("[Info] - ")
logger.SetFlags(log.Ldate|log.Ltime|log.LUTC)
logger.Print("Print")
logger.Println("Println")
}執(zhí)行以上代碼,得到 demo.log 日志內(nèi)容如下:
main.go:15: [Debug] - Print
main.go:16: [Debug] - Println
控制臺輸出內(nèi)容如下:
[Info] - 2023/03/11 01:24:56 Print
[Info] - 2023/03/11 01:24:56 Println
可以發(fā)現(xiàn),在 demo.log 日志內(nèi)容中,因為指定了 log.Lmsgprefix 屬性,所以日志前綴 [Debug] - 被移動到了日志內(nèi)容前面,而非行首。
因為后續(xù)通過 logger.SetXXX 對 logger 對象的屬性進行了動態(tài)修改,所以最后兩條日志輸出到系統(tǒng)的標準輸出。
以上,基本涵蓋了 log 包的所有常用功能。接下來我們就通過走讀源碼的方式來更深入的了解 log 包了。
源碼
注意:本文以 Go 1.19.4 源碼為例,其他版本可能存在差異。
Go 標準庫的 log 包代碼量非常少,算上注釋也才 400+ 行,非常適合初學者閱讀學習。
在上面介紹的第一個示例中,我們使用 log 包提供的 9 個公開函數(shù)對日志進行輸出,并通過表格的形式分別介紹了函數(shù)的作用和使用示例,那么現(xiàn)在我們就來看看這幾個函數(shù)是如何定義的:
func Print(v ...any) {
if atomic.LoadInt32(&std.isDiscard) != 0 {
return
}
std.Output(2, fmt.Sprint(v...))
}
func Printf(format string, v ...any) {
if atomic.LoadInt32(&std.isDiscard) != 0 {
return
}
std.Output(2, fmt.Sprintf(format, v...))
}
func Println(v ...any) {
if atomic.LoadInt32(&std.isDiscard) != 0 {
return
}
std.Output(2, fmt.Sprintln(v...))
}
func Fatal(v ...any) {
std.Output(2, fmt.Sprint(v...))
os.Exit(1)
}
func Fatalf(format string, v ...any) {
std.Output(2, fmt.Sprintf(format, v...))
os.Exit(1)
}
func Fatalln(v ...any) {
std.Output(2, fmt.Sprintln(v...))
os.Exit(1)
}
func Panic(v ...any) {
s := fmt.Sprint(v...)
std.Output(2, s)
panic(s)
}
func Panicf(format string, v ...any) {
s := fmt.Sprintf(format, v...)
std.Output(2, s)
panic(s)
}
func Panicln(v ...any) {
s := fmt.Sprintln(v...)
std.Output(2, s)
panic(s)
}可以發(fā)現(xiàn),這些函數(shù)代碼主邏輯基本一致,都是通過 std.Output 輸出日志。不同的是,PrintX 輸出日志后程序就執(zhí)行結(jié)束了;Fatal 輸出日志后會執(zhí)行 os.Exit(1);而 Panic 輸出日志后會執(zhí)行 panic(s)。
那么接下來就是要搞清楚這個 std 對象是什么,以及它的 Output 方法是如何定義的。
我們先來看下 std 是什么:
var std = New(os.Stderr, "", LstdFlags)
func New(out io.Writer, prefix string, flag int) *Logger {
l := &Logger{out: out, prefix: prefix, flag: flag}
if out == io.Discard {
l.isDiscard = 1
}
return l
}可以看到,std 其實就是使用 New 創(chuàng)建的一個 Logger 對象,日志輸出到標準錯誤輸出,日志前綴為空,日志屬性為 LstdFlags。
這跟我們上面講的自定義日志對象 logger := log.New(os.Stdout, "[Debug] - ", log.Lshortfile) 方式如出一轍。也就是說,當我們通過 log.Print("Print") 打印日志時,其實使用的是 log 包內(nèi)部已經(jīng)定義好的 Logger 對象。
Logger 定義如下:
type Logger struct {
mu sync.Mutex // 鎖,保證并發(fā)情況下對其屬性操作是原子性的
prefix string // 日志前綴,即 Lmsgprefix 參數(shù)值
flag int // 日志屬性,用來控制日志輸出格式
out io.Writer // 日志輸出位置,實現(xiàn)了 io.Writer 接口即可,如 文件、os.Stderr
buf []byte // 存儲日志輸出內(nèi)容
isDiscard int32 // 當 out = io.Discard 是,此值為 1
}其中,flag 和 isDiscard 這兩個屬性有必要進一步解釋下。
首先是 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 )
具體含義我就不再一一解釋了,前文的表格已經(jīng)寫的很詳細了。
值得注意的是,這里在定義常量時,巧妙的使用了左移運算符 1 << iota,使得常量的值呈現(xiàn) 1、2、4、8... 這樣的遞增效果。其實是為了位運算方便,通過對屬性進行位運算,來決定輸出內(nèi)容,其本質(zhì)上跟基于位運算的權(quán)限管理是一樣的。所以在使用 log.New 新建 Logger 對象時可以支持 log.Ldate|log.Ltime|log.Lshortfile 這種形式設(shè)置多個屬性。
std 對象的屬性初始值 LstdFlags 也是在這里定義的。
其次還有一個屬性 isDiscard,是用來丟棄日志的。在上面介紹 PrintX 函數(shù)定義時,在輸出日志前有一個 if atomic.LoadInt32(&std.isDiscard) != 0 的判斷,如果結(jié)果為真,則直接 return 不記錄日志。
在 Go 標準庫的 io 包里,有一個 io.Discard 對象,io.Discard 實現(xiàn)了 io.Writer,它執(zhí)行 Write 操作后不會產(chǎn)生任何實際的效果,是一個用于丟棄數(shù)據(jù)的對象。比如有時候我們不在意數(shù)據(jù)內(nèi)容,但可能存在數(shù)據(jù)不讀出來就無法關(guān)閉連接的情況,這時候就可以使用 io.Copy(io.Discard, io.Reader) 將數(shù)據(jù)寫入 io.Discard 實現(xiàn)丟棄數(shù)據(jù)的效果。
使用 New 創(chuàng)建 Logger 對象時,如果 out == io.Discard 則 l.isDiscard 的值會被置為 1,所以使用 PrintX 函數(shù)記錄的日志將會被丟棄,而 isDiscard 屬性之所以是 int32 類型而不是 bool,是因為方便原子操作。
現(xiàn)在,我們終于可以來看 std.Output 的實現(xiàn)了:
func (l *Logger) Output(calldepth int, s string) error {
now := time.Now() // 獲取當前時間
var file string
var line int
// 加鎖,保證并發(fā)安全
l.mu.Lock()
defer l.mu.Unlock()
// 通過位運算來判斷是否需要獲取文件名和行號
if l.flag&(Lshortfile|Llongfile) != 0 {
// 運行 runtime.Caller 獲取文件名和行號比較耗時,所以先釋放鎖
l.mu.Unlock()
var ok bool
_, file, line, ok = runtime.Caller(calldepth)
if !ok {
file = "???"
line = 0
}
// 獲取到文件行號后再次加鎖,保證下面代碼并發(fā)安全
l.mu.Lock()
}
// 清空上次緩存的內(nèi)容
l.buf = l.buf[:0]
// 格式化日志頭信息(如:日期時間、文件名和行號、前綴)并寫入 buf
l.formatHeader(&l.buf, now, file, line)
// 追加日志內(nèi)容到 buf
l.buf = append(l.buf, s...)
// 保證輸出日志以 \n 結(jié)尾
if len(s) == 0 || s[len(s)-1] != '\n' {
l.buf = append(l.buf, '\n')
}
// 調(diào)用 Logger 對象的 out 屬性的 Write 方法輸出日志
_, err := l.out.Write(l.buf)
return err
}Output 方法代碼并不多,基本邏輯也比較清晰,首先根據(jù)日志屬性來決定是否需要獲取文件名和行號,因為調(diào)用 runtime.Caller 是一個耗時操作,開銷比較大,為了增加并發(fā)性,暫時將鎖釋放,獲取到文件名和行號后再重新加鎖。
接下來就是準備日志輸出內(nèi)容了,首先清空 buf 中保留的上次日志信息,然后通過 formatHeader 方法格式化日志頭信息,接著把日志內(nèi)容也追加到 buf 中,在這之后有一個保證輸出日志以 \n 結(jié)尾的邏輯,來保證輸出的日志都是單獨一行的。不知道你有沒有注意到,在前文的 log 包使用示例中,我們使用 Print 和 Println 兩個方法時,最終日志輸出效果并無差別,使用 Print 打印日志也會換行,其實就是這里的邏輯決定的。
最后,通過調(diào)用 l.out.Write 方法,將 buf 內(nèi)容輸出。
我們來看下用來格式化日志頭信息的 formatHeader 方法是如何定義:
func (l *Logger) formatHeader(buf *[]byte, t time.Time, file string, line int) {
// 如果沒有設(shè)置 Lmsgprefix 屬性,將日志前綴內(nèi)容設(shè)置到行首
if l.flag&Lmsgprefix == 0 {
*buf = append(*buf, l.prefix...)
}
// 判斷是否設(shè)置了日期時間相關(guān)的屬性
if l.flag&(Ldate|Ltime|Lmicroseconds) != 0 {
// 是否設(shè)置 UTC 時間
if l.flag&LUTC != 0 {
t = t.UTC()
}
// 是否設(shè)置日期
if l.flag&Ldate != 0 {
year, month, day := t.Date()
itoa(buf, year, 4)
*buf = append(*buf, '/')
itoa(buf, int(month), 2)
*buf = append(*buf, '/')
itoa(buf, day, 2)
*buf = append(*buf, ' ')
}
// 是否設(shè)置時間
if l.flag&(Ltime|Lmicroseconds) != 0 {
hour, min, sec := t.Clock()
itoa(buf, hour, 2)
*buf = append(*buf, ':')
itoa(buf, min, 2)
*buf = append(*buf, ':')
itoa(buf, sec, 2)
if l.flag&Lmicroseconds != 0 {
*buf = append(*buf, '.')
itoa(buf, t.Nanosecond()/1e3, 6)
}
*buf = append(*buf, ' ')
}
}
// 是否設(shè)置文件名和行號
if l.flag&(Lshortfile|Llongfile) != 0 {
if l.flag&Lshortfile != 0 {
short := file
for i := len(file) - 1; i > 0; i-- {
if file[i] == '/' {
short = file[i+1:]
break
}
}
file = short
}
*buf = append(*buf, file...)
*buf = append(*buf, ':')
itoa(buf, line, -1)
*buf = append(*buf, ": "...)
}
// 如果設(shè)置了 Lmsgprefix 屬性,將日志前綴內(nèi)容放到日志頭信息最后,也就是緊挨著日志內(nèi)容前面
if l.flag&Lmsgprefix != 0 {
*buf = append(*buf, l.prefix...)
}
}formatHeader 方法是 log 包里面代碼量最多的一個方法,主要通過按位與(&)來計算是否設(shè)置了某個日志屬性,然后根據(jù)日志屬性來格式化頭信息。
其中多次調(diào)用 itoa 函數(shù),itoa 顧名思義,將 int 轉(zhuǎn)換成 ASCII 碼,itoa 定義如下:
func itoa(buf *[]byte, i int, wid int) {
// Assemble decimal in reverse order.
var b [20]byte
bp := len(b) - 1
for i >= 10 || wid > 1 {
wid--
q := i / 10
b[bp] = byte('0' + i - q*10)
bp--
i = q
}
// i < 10
b[bp] = byte('0' + i)
*buf = append(*buf, b[bp:]...)
}這個函數(shù)短小精悍,它接收三個參數(shù),buf 用來保存轉(zhuǎn)換后的內(nèi)容,i 就是帶轉(zhuǎn)換的值,比如 year、month 等,wid 表示轉(zhuǎn)換后 ASCII 碼字符串寬度,如果傳進來的 i 寬度不夠,則前面補零。比如傳入 itoa(&b, 12, 3),最終輸出字符串為 012。
至此,我們已經(jīng)理清了 log.Print("Print") 是如何打印一條日志的,其函數(shù)調(diào)用流程如下:

上面我們講解了使用 log 包中默認的 std 這個 Logger 對象打印日志的調(diào)用流程。當我們使用自定義的 Logger 對象(logger := log.New(os.Stdout, "[Debug] - ", log.Lshortfile))來打印日志時,調(diào)用的 loggger.Print 是一個方法,而不是 log.Print 這個包級別的函數(shù),所以其實 Logger 結(jié)構(gòu)體也實現(xiàn)了 9 種輸出日志方法:
func (l *Logger) Print(v ...any) {
if atomic.LoadInt32(&l.isDiscard) != 0 {
return
}
l.Output(2, fmt.Sprint(v...))
}
func (l *Logger) Printf(format string, v ...any) {
if atomic.LoadInt32(&l.isDiscard) != 0 {
return
}
l.Output(2, fmt.Sprintf(format, v...))
}
func (l *Logger) Println(v ...any) {
if atomic.LoadInt32(&l.isDiscard) != 0 {
return
}
l.Output(2, fmt.Sprintln(v...))
}
func (l *Logger) Fatal(v ...any) {
l.Output(2, fmt.Sprint(v...))
os.Exit(1)
}
func (l *Logger) Fatalf(format string, v ...any) {
l.Output(2, fmt.Sprintf(format, v...))
os.Exit(1)
}
func (l *Logger) Fatalln(v ...any) {
l.Output(2, fmt.Sprintln(v...))
os.Exit(1)
}
func (l *Logger) Panic(v ...any) {
s := fmt.Sprint(v...)
l.Output(2, s)
panic(s)
}
func (l *Logger) Panicf(format string, v ...any) {
s := fmt.Sprintf(format, v...)
l.Output(2, s)
panic(s)
}
func (l *Logger) Panicln(v ...any) {
s := fmt.Sprintln(v...)
l.Output(2, s)
panic(s)
}這 9 個方法跟 log 包級別的函數(shù)一一對應,用于自定義 Logger 對象。
有一個值得注意的點,在這些方法內(nèi)部,調(diào)用 l.Output(2, s) 時,第一個參數(shù) calldepth 傳遞的是 2,這跟 runtime.Caller(calldepth) 函數(shù)內(nèi)部實現(xiàn)有關(guān),runtime.Caller 函數(shù)簽名如下:
func Caller(skip int) (pc uintptr, file string, line int, ok bool)
runtime.Caller 返回當前 Goroutine 的棧上的函數(shù)調(diào)用信息(程序計數(shù)器、文件信息、行號),其參數(shù) skip 表示當前向上層的棧幀數(shù),0 代表當前函數(shù),也就是調(diào)用 runtime.Caller 的函數(shù),1 代表上一層調(diào)用者,以此類推。
因為函數(shù)調(diào)用鏈為 main.go -> log.Print -> std.Output -> runtime.Caller,所以 skip 值即為 2:
- 0: 表示
std.Output中調(diào)用runtime.Caller所在的源碼文件和行號。 - 1: 表示
log.Print中調(diào)用std.Output所在的源碼文件和行號。 - 2: 表示
main.go中調(diào)用log.Print所在的源碼文件和行號。
這樣當代碼出現(xiàn)問題時,就能根據(jù)日志中記錄的函數(shù)調(diào)用棧來找到報錯的源碼位置了。
另外,前文介紹過三個設(shè)置 Logger 對象屬性的方法,分別是 SetOutput、SetPrefix、SetFlags,其實這三個方法各自還有與之對應的獲取相應屬性的方法,定義如下:
func (l *Logger) Flags() int {
l.mu.Lock()
defer l.mu.Unlock()
return l.flag
}
func (l *Logger) SetFlags(flag int) {
l.mu.Lock()
defer l.mu.Unlock()
l.flag = flag
}
func (l *Logger) Prefix() string {
l.mu.Lock()
defer l.mu.Unlock()
return l.prefix
}
func (l *Logger) SetPrefix(prefix string) {
l.mu.Lock()
defer l.mu.Unlock()
l.prefix = prefix
}
func (l *Logger) Writer() io.Writer {
l.mu.Lock()
defer l.mu.Unlock()
return l.out
}
func (l *Logger) SetOutput(w io.Writer) {
l.mu.Lock()
defer l.mu.Unlock()
l.out = w
isDiscard := int32(0)
if w == io.Discard {
isDiscard = 1
}
atomic.StoreInt32(&l.isDiscard, isDiscard)
}其實就是針對每個私有屬性,定義了 getter、setter 方法,并且每個方法內(nèi)部為了保證并發(fā)安全,都進行了加鎖操作。
當然,log 包級別的函數(shù),也少不了這幾個功能:
func Default() *Logger { return std }
func SetOutput(w io.Writer) {
std.SetOutput(w)
}
func Flags() int {
return std.Flags()
}
func SetFlags(flag int) {
std.SetFlags(flag)
}
func Prefix() string {
return std.Prefix()
}
func SetPrefix(prefix string) {
std.SetPrefix(prefix)
}
func Writer() io.Writer {
return std.Writer()
}
func Output(calldepth int, s string) error {
return std.Output(calldepth+1, s) // +1 for this frame.
}至此,log 包的全部代碼我們就一起走讀完成了。
總結(jié)一下:log 包主要設(shè)計了 Logger 對象和其方法,并且為了開箱即用,在包級別又對外提供了默認的 Logger 對象 std 和一些包級別的對外函數(shù)。Logger 對象的方法,和包級別的函數(shù)基本上是一一對應的,簽名一樣,這樣就大大降低了使用難度。
使用建議
關(guān)于 log 包的使用,我還有幾條建議分享給你:
log 默認不支持 Debug、Info、Warn 等更細粒度級別的日志輸出方法,不過我們可以通過創(chuàng)建多個 Logger 對象的方式來實現(xiàn),只需要給每個 Logger 對象指定不同的日志前綴即可:
loggerDebug = log.New(os.Stdout, "[Debug]", log.LstdFlags) loggerInfo = log.New(os.Stdout, "[Info]", log.LstdFlags) loggerWarn = log.New(os.Stdout, "[Warn]", log.LstdFlags) loggerError = log.New(os.Stdout, "[Error]", log.LstdFlags)
- 僅在
main.go文件中使用log.Panic、log.Fatal,下層程序遇到錯誤時先記錄日志,然后將錯誤向上一層層拋出,直到調(diào)用棧頂層才決定要不要使用log.Panic、log.Fatal。 - log 包作為 Go 標準庫,僅支持日志的基本功能,不支持記錄結(jié)構(gòu)化日志、日志切割、Hook 等高級功能,所以更適合小型項目使用,比如一個單文件的腳本。對于中大型項目,則推薦使用一些主流的第三方日志庫,如 logrus、zap、glog 等。
- 另外,如果你對 Go 標準日志庫有所期待,Go 官方還打造了另一個日志庫 slog 現(xiàn)已進入實驗階段,如果項目發(fā)展順利,將可能成為 log 包的替代品。
以上就是深入探究Golang中l(wèi)og標準庫的使用的詳細內(nèi)容,更多關(guān)于Golang log標準庫的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
詳解Go如何實現(xiàn)協(xié)程并發(fā)執(zhí)行
線程是通過本地隊列,全局隊列或者偷其它線程的方式來獲取協(xié)程的,目前看來,線程運行完一個協(xié)程后再從隊列中獲取下一個協(xié)程執(zhí)行,還只是順序執(zhí)行協(xié)程的,而多個線程一起這么運行也能達到并發(fā)的效果,接下來就給給大家詳細介紹一下Go如何實現(xiàn)協(xié)程并發(fā)執(zhí)行2023-08-08
go內(nèi)存緩存如何new一個bigcache對象示例詳解
這篇文章主要為大家介紹了go內(nèi)存緩存如何new一個bigcache對象示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2023-09-09
Go|使用Options模式和建造者模式創(chuàng)建對象實戰(zhàn)
這篇文章主要介紹了Go使用Options模式和建造者模式創(chuàng)建對象實戰(zhàn)詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2023-04-04

