这篇文章将开始学习 go 标准库提供的 log 功能。log
包提供了简单的、格式化日志输出功能,log/syslog
包则提供了访问系统日志服务(syslog)的能力,而 log/slog
包则提供了结构化日志功能。
这篇文章将首先学习 log
和 log/syslog
包。
log 包 go 标准库的 log
包实现了简易的日志功能。log 库定义了 Logger
类型,该类型提供了一些方法来进行格式化输出。同时为了简化使用、省去手动创建 Logger 对象的步骤,该包也预定义了一个 标准的 logger 对象
,可以直接通过工具函数 Print[f|ln]
、Fatal[f|ln]
、Panic[f|ln]
来使用该 logger
对象。
Logger 类型 Logger
类型用于表示一个日志对象,通过日志对象即可将日志消息写入到指定的 io.Writer
中。Logger
类型的定义如下,它可以用于并发环境,即可在同时在多个 goroutine 中使用同一个 Logger
对象,Logger
内部能够确保序列化地访问 Writer。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 type Logger struct { outMu sync.Mutex out io.Writer prefix atomic.Pointer[string ] flag atomic.Int32 isDiscard atomic.Bool }
Logger 对象的创建 log.New
函数用于创建一个 Logger:
1 2 3 4 5 6 7 8 9 10 11 12 13 func New (out io.Writer, prefix string , flag int ) *Logger { l := new (Logger) l.SetOutput(out) l.SetPrefix(prefix) l.SetFlags(flag) return l }
flag 标志可以是如下值,用于控制日志对象的日志输出行为:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 const ( Ldate = 1 << iota Ltime Lmicroseconds Llongfile Lshortfile LUTC Lmsgprefix LstdFlags = Ldate | Ltime )
日志输出的实现 日志输出的核心实现是在 Logger.output
函数中,它的实现如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 func (l *Logger) output(pc uintptr , calldepth int , appendOutput func ([]byte ) []byte ) error { if l.isDiscard.Load() { return nil } now := time.Now() prefix := l.Prefix() flag := l.Flags() var file string var line int if flag&(Lshortfile|Llongfile) != 0 { if pc == 0 { var ok bool _, file, line, ok = runtime.Caller(calldepth) if !ok { file = "???" line = 0 } } else { fs := runtime.CallersFrames([]uintptr {pc}) f, _ := fs.Next() file = f.File if file == "" { file = "???" } line = f.Line } } buf := getBuffer() defer putBuffer(buf) formatHeader(buf, now, prefix, flag, file, line) *buf = appendOutput(*buf) if len (*buf) == 0 || (*buf)[len (*buf)-1 ] != '\n' { *buf = append (*buf, '\n' ) } l.outMu.Lock() defer l.outMu.Unlock() _, err := l.out.Write(*buf) return err }
为了获取日志输出点(即调用日志输出函数的代码位置)的文件名、行号信息,log
包使用 runtime
包,并通过 calldepth
或者 pc
值来获取这些信息
如果 pc 值为 0,则通过 runtime.Caller(calldepth)
来获取。calleddepth 表示调用栈中 日志输出函数
调用点距离 runtime.Caller
调用点的深度。
如果指定了 pc 值,则通过 runtime.CallersFrames([]uintptr{pc})
并通过 Frames.Next()
来获取,因此 pc 的值应该是调用栈中 日志输出函数
调用点所在函数的 pc 值
runtime.Caller(skip)
中的 skip
参数表示调用栈帧的层次,如果为 0 则表示调用 runtime.Caller
的函数所在的栈帧,如果为 1,则表示调用链更上一层函数所在的栈帧。如果是一个简单示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 package mainfunc f1 () { f2() } func f2 () { f3() } func main () { println ("caller test start" ) f1() println ("caller test end" ) }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 package mainimport ( "fmt" "runtime" ) func f3 () { f4() f5() } func f4 () { fmt.Println(runtime.Caller(0 )) fmt.Println(runtime.Caller(1 )) fmt.Println(runtime.Caller(2 )) fmt.Println(runtime.Caller(3 )) fmt.Println(runtime.Caller(4 )) fmt.Println(runtime.Caller(5 )) } func f5 () { pc, _, _, _ := runtime.Caller(0 ) frames := runtime.CallersFrames([]uintptr {pc}) for { f, more := frames.Next() fmt.Println(f.File, f.Line, f.Func.Name()) if !more { break } } }
1 2 3 4 5 6 7 8 9 10 # ./caller caller test start 4780932 /root/code/private/go /go_std/log/caller/callee.go 17 true 4780878 /root/code/private/go /go_std/log/caller/callee.go 10 true 4783120 /root/code/private/go /go_std/log/caller/caller.go 9 true 4783115 /root/code/private/go /go_std/log/caller/caller.go 5 true 4783114 /root/code/private/go /go_std/log/caller/caller.go 14 true 4412106 /usr/local/go /src/runtime/proc.go 272 true /root/code/private/go /go_std/log/caller/callee.go 26 main.f5 caller test end
output
函数的 appendOutput
参数是一个回调函数,用于将日志消息内容添加到目标缓冲区中。因为 logger 库提供的 API 不同,日志消息内容的添加方式不同,因此通过定义一个回调函数,让 appendOutput 自己处理如何将日志消息内容添加到 buf 中 。
logger
库为了高效地管理日志输出所使用的缓冲区,定义了一个 bufferPool
,getBuffer
函数用于从该 Pool 获取一个 buffer,在将日志消息写入到 io.Writer 后,通过 putBuffer
函数将该 buffer 放回 Pool 中:
1 var bufferPool = sync.Pool{New: func () any { return new ([]byte ) }}
一个完整日志消息的构造可以分为如下三个步骤:
formatHeader(buf, now, prefix, flag, file, line) 用于在 buf 中添加日志消息头
通过 appendOutput
添加日志消息体
最后会判断日志消息的末尾是否存在换行符,如果没有,自动添加一个换行符,以保证每条日志消息总是单独一行
在构造完日志消息后,就是调用 l.out.Write(*buf)
输出日志消息了。
日志输出 API 在学习日志输出的核心实现后,再来看 logger 库提供的日志输出 API。由于这些日志输出 API 都是借助 fmt 库中的格式化输出函数来构造日志消息体,因此和 fmt 库的 API 类似,提供三种 API 的变体:XXX
、XXXln
和 XXXf
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 func (l *Logger) Print(v ...any)func (l *Logger) Println(v ...any)func (l *Logger) Printf(format string , v ...any)func (l *Logger) Fatal(v ...any)func (l *Logger) Fatalln(v ...any)func (l *Logger) Fatalf(format string , v ...any)func (l *Logger) Panic(v ...any)func (l *Logger) Panicln(v ...any)func (l *Logger) Panicf(format string , v ...any)func (l *Logger) Output(calldepth int , s string ) error
Print 系列函数用于单纯输出日志消息
Fatal 系列函数在输出日志消息后,会调用 os.Exit(1)
退出程序
Panic 系列函数在输出日志消息后,会以日志消息为参数,触发 panic 流程
Output 函数可以让我们更自由地输出日志,尤其是 calledepth 参数,可以让我们对日志输出函数进行包装的同时,正确地输出 日志输出调用点
的文件名、行信息,如下是一个示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 package mainimport ( "fmt" "log" "os" ) func PrintWithPrefix (l *log.Logger, prefix string , v ...any) { l.Print(append ([]any{prefix}, v...)...) } func OutputWithPrefix (l *log.Logger, prefix string , v ...any) { l.Output(2 , fmt.Sprint(append ([]any{prefix}, v...)...)) } func main () { l := log.New(os.Stdout, "" , log.LstdFlags|log.Lshortfile) PrintWithPrefix(l, "wrapper logger" , "log message 1" ) PrintWithPrefix(l, "wrapper logger" , "log message 2" ) OutputWithPrefix(l, "wrapper logger" , "log message 3" ) OutputWithPrefix(l, "wrapper logger" , "log message 4" ) }
1 2 3 4 5 # go run wrap_log.go 2024 /12 /07 19 :15 :00 wrap_log.go :10 : wrapper loggerlog message 1 2024 /12 /07 19 :15 :00 wrap_log.go :10 : wrapper loggerlog message 2 2024 /12 /07 19 :15 :00 wrap_log.go :22 : wrapper loggerlog message 3 2024 /12 /07 19 :15 :00 wrap_log.go :23 : wrapper loggerlog message 4
可以看到,在对日志输出函数进行包裹后,PrintWithPrefix
总是打印 Print
函数所在的文件行信息,而 OutputWithPrefix
则可以正确打印日志输出函数调用点所在的文件行信息。
Logger 类型的其他方法 通过上面 Logger
类型的定义可以看到,其内部实现都是非导出字段,因此 Logger
类型也提供了一系列方法,来获取和设置日志对象的各种属性。下面简单列举如下:
1 2 3 4 5 6 7 8 9 10 11 12 func (l *Logger) Flags() int func (l *Logger) SetFlags(flag int )func (l *Logger) Prefix() string func (l *Logger) SetPrefix(prefix string )func (l *Logger) Writer() io.Writerfunc (l *Logger) SetOutput(w io.Writer)
标准 Logger 对象 为了简化 log 包的使用,log 包预定义了一个 标准 Logger 对象
。从它的创建方式可以看出,标准 logger 对象
将日志写入到标准错误上,并打印每条日志消息的日期时间,
1 2 3 4 5 const ( LstdFlags = Ldate | Ltime ) var std = New(os.Stderr, "" , LstdFlags)
如下是 log 包提供的顶层系列函数,这些函数操作的就是该 标准 Logger 对象
,其行为与 Logger
类型的对应方法相同,这里就不再赘述:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 func Default () *Logger { return std }func Print (v ...any) func Println (v ...any) func Printf (format string , v ...any) func Fatal (v ...any) func Fatalln (v ...any) func Fatalf (format string , v ...any) func Panic (v ...any) func Panicln (v ...any) func Panicf (format string , v ...any) func Output (calldepth int , s string ) error func Flags () int func SetFlags (flag int ) func Prefix () string func SetPrefix (prefix string ) func Writer () io.Writerfunc SetOutput (w io.Writer)
如下是使用 标准 Logger 对象
的简单示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 package mainimport "log" func main () { s := "test log message" log.Printf(s) stdLogger := log.Default() stdLogger.SetPrefix("prefix/" ) log.Printf(s) stdLogger.SetFlags(log.LstdFlags | log.Lmsgprefix) log.Printf(s) }
1 2 3 4 # go run msgprefix.go 2024 /12 /07 11 :35 :13 test log messageprefix/2024 /12 /07 11 :35 :13 test log message 2024 /12 /07 11 :35 :13 prefix/test log message
syslog 包 标准库的 log/syslog
包提供了访问系统 log 服务的简易接口,它可以通过 UNIX socket、UDP 或者 TCP 将日志消息发送给系统的 syslog 守护进程。在和系统 syslog 服务建立连接时,只需要 Dial
一次。如果出现写入失败,syslog 客户端会自动尝试重连到 Server。当前 syslog 包已经处于冻结状态,不会再接收新特性。
创建 syslog 客户端 有两种方式来向 syslog 服务发送日志,一种是仍然通过 logger
包提供的 API 来发送日志,另一种是通过 syslog.Writer
对象来写入日志。
syslog.NewLogger
函数返回一个 *log.Logger
对象,通过该对象,我们仍然可以使用熟悉的日志 API 来发送日志,只不过该日志对象的底层 Writer 是和本机 syslog 服务建立的连接。需要注意,日志 API 发送日志消息时,添加的消息头会作为整个 syslog
消息体的一部分存在。
1 2 3 4 5 6 7 8 9 10 func NewLogger (p Priority, logFlag int ) (*log.Logger, error ) { s, err := New(p, "" ) if err != nil { return nil , err } return log.New(s, "" , logFlag), nil }
另外一种是直接通过 syslog.Writer
对象来写入日志,syslog.Writer
的类型定义如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 type Writer struct { priority Priority tag string hostname string network string raddr string mu sync.Mutex conn serverConn }
可以通过如下两个函数创建来创建 syslog.Writer
对象:
1 2 3 4 5 func New (priority Priority, tag string ) (*Writer, error )func Dial (network, raddr string , priority Priority, tag string ) (*Writer, error )
priority 参数用于指定 SYSLOG 日志的优先级,它是 Facility
和 Severity
的结合值,其中 Facility 是日志消息的来源,Severity 是日志消息的重要性。
tag 参数用来区分日志消息,如果没有指定,默认会使用 [os.Args][0]
,即程序名
在得到 syslog.Writer
对象后,就可以通过如下函数直接向 syslog 输出日志消息:
1 2 3 4 5 6 7 8 9 10 11 12 func (w *Writer) Write(b []byte ) (int , error )func (w *Writer) Emerg(m string ) error func (w *Writer) Alert(m string ) error func (w *Writer) Crit(m string ) error func (w *Writer) Err(m string ) error func (w *Writer) Warning(m string ) error func (w *Writer) Notice(m string ) error func (w *Writer) Info(m string ) error func (w *Writer) Debug(m string ) error
一些实现细节 unixSyslog
函数用于和本机 syslog 服务建立连接,如下是它的实现,可以看出其是如何和本机 syslog 服务建立的连接的:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 func unixSyslog () (conn serverConn, err error ) { logTypes := []string {"unixgram" , "unix" } logPaths := []string {"/dev/log" , "/var/run/syslog" , "/var/run/log" } for _, network := range logTypes { for _, path := range logPaths { conn, err := net.Dial(network, path) if err == nil { return &netConn{conn: conn, local: true }, nil } } } return nil , errors.New("Unix syslog delivery error" ) }
而如下函数则用于将待发送的日志消息格式化成 SYSLOG 格式的消息进行发送,SYSLOG 消息格式为:<PRI>TIMESTAMP HOSTNAME TAG[PID]: MSG
1 func (n *netConn) writeString(p Priority, hostname, tag, msg, nl string )
简单示例 如下是使用 log/syslog
包向本机 syslog 服务发送日志消息的示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 package mainimport ( "log" "log/syslog" ) func main () { l, err := syslog.NewLogger(syslog.LOG_USER|syslog.LOG_INFO, log.LstdFlags) if err != nil { return } l.Println("syslog message 1" ) w := l.Writer().(*syslog.Writer) w.Alert("syslog message 2" ) }
1 2 3 4 # more /var /log/syslog ...... Dec 7 20 :30 :03 OPS-3430 ./main[2363580 ]: 2024 /12 /07 20 :30 :03 syslog message 1 Dec 7 20 :30 :03 OPS-3430 ./main[2363580 ]: syslog message 2
总结 本文介绍了 go 标准库 log
包和 log/syslog
包的基本使用方法和一些实现细节,log
包提供了简单易用的日志输出功能,而 log/syslog
包则提供了向 syslog 服务发送日志的能力。它们为 Gopher 提供了最基本的日志功能。