这篇文章将开始学习 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 提供了最基本的日志功能。