log包实现了简单的日志打印功能,支持日志输出到控制台或者日志文件。log包里核心的数据结构只有1个Logger,定义如下
// A Logger represents an active logging object that generates lines of
// output to an io.Writer. Each logging operation makes a single call to
// the Writer's Write method. A Logger can be used simultaneously from
// multiple goroutines; it guarantees to serialize access to the Writer.
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
}
Logger结构体里的字段,在使用上我们只需要关心prefix,flag和out这3个字段的含义:
-
out:表示日志输出的地方。可以是标准输出os.Stdout,os.Stderr或者指定的本地文件
-
flag:日志的属性设置。表示每行日志最开头打印的内容。取值如下:
// These flags define which text to prefix to each log entry generated by the Logger. // Bits are or'ed together to control what's printed. // With the exception of the Lmsgprefix flag, there is no // control over the order they appear (the order listed here) // or the format they present (as described in the comments). // The prefix is followed by a colon only when Llongfile or Lshortfile // is specified. // For example, flags Ldate | Ltime (or LstdFlags) produce, // 2009/01/23 01:23:23 message // while flags Ldate | Ltime | Lmicroseconds | Llongfile produce, // 2009/01/23 01:23:23.123123 /a/b/c/d.go:23: message 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 )
-
prefix:每行日志最开头的日志前缀
注意:如果flag开启了Lmsgprefix,那这个prefix前缀就不是放在每行日志的最开头了,而是在具体被打印的内容的前面。比如prefix如果是"INFO:"
-
flag不开启Lmsgprefix的时候,prefix在每行日志最开头,日志输出为:
INFO:2021/12/01 21:00:34 example1.go:14: your message
-
flag开启Lmsgprefix的时候,prefix在要打印的内容"your message"的前面,日志输出为:
2021/12/01 21:02:20 example1.go:14: INFO:your message
-
Logger结构体实现了若干指针接收者方法,包括设置日志属性、打印日志等。
同时在log这个包里,自带了一个默认的Logger,源码定义如下:
var std = New(os.Stderr, "", LstdFlags)
这个自带的std配套有若干辅助函数,用于设置日志属性和打印日志等。
这些辅助函数实际上就是对Logger结构体的方法做了一层封装,在辅助函数里面都是通过std这个Logger指针去调用Logger的方法。所以辅助函数和Logger结构体方法是一一对应的。
要使用log包打印日志,有2种方式,可以根据各自业务场景选择对应方法:
- 方法1:使用log包里自带的std这个Logger指针。通常用于在控制台输出日志。
- 方法2:自定义Logger。通常用于把日志输出到文件里。
方法1和方法2相比,没有本质区别,只是使用场景上有一个偏好。
当然方法1也可以实现输出日志到文件里,方法2也可以实现在控制台打印日志。
下面详细介绍下这两种方式的用法。
talk is cheap, show me the code。我们先看一段代码示例:
// example1.go
package main
import (
"log"
)
func main() {
// 通过SetFlags设置Logger结构体里的flag属性
log.SetFlags(log.Ldate | log.Ltime | log.Lshortfile | log.Lmsgprefix)
// 通过SetPrefix设置Logger结构体里的prefix属性
log.SetPrefix("INFO:")
// 调用辅助函数Println打印日志到标准
log.Println("your message")
}
上面的示例,使用了log包里自带的std标准输出,先通过SetFlags和SetPrefix这2个log包里的函数设置好std指向的Logger结构体对象里的flag和prefix属性,然后通过log包里定义的Println函数,把日志打印到控制台。程序运行结果如下:
2021/12/01 18:18:53 example3.go:14: INFO:your message
总结方式1的使用流程如下:
-
通过调用SetFlags,SetPrefix,SetOutput函数设置好日志属性。SetOutPut可以用于设置日志输出的地方,比如终端,文件等。
如果省略这个步骤,会使用std创建时设置的默认属性。我们回顾下std的创建代码:
// New creates a new Logger. The out variable sets the // destination to which log data will be written. // The prefix appears at the beginning of each generated log line, or // after the log header if the Lmsgprefix flag is provided. // The flag argument defines the logging properties. func New(out io.Writer, prefix string, flag int) *Logger { return &Logger{out: out, prefix: prefix, flag: flag} } var std = New(os.Stderr, "", LstdFlags)
从上面的源码可以看出std是默认把日志输出到控制台,默认日志的prefix前缀为空串,默认flag属性是LstdFlags,也就是日志开头会打印日期和时间,比如:2009/01/23 01:23:23
调用SetXXX函数可以修改std的默认设置。
-
调用log包里的辅助函数Print[f|ln],Fatal[f|ln],Panic[f|ln]打印日志
- Fatal[f|ln]打印日志后会调用os.Exit(1)
- Panic[f|ln]打印日志后会调用panic
上面的例子example1.go是使用log包自带的std这个Logger指针把日志输出到控制台,我们也可以使用std把日志输出到指定文件,调用SetOutput设置日志输出的参数即可。参见如下代码示例:
// example2.go
package main
import (
"fmt"
"log"
"os"
"time"
)
func main() {
// 日志文件名
fileName := fmt.Sprintf("app_%s.log", time.Now().Format("20060102"))
// 创建文件
f, err := os.OpenFile(fileName, os.O_RDWR | os.O_CREATE | os.O_APPEND, 0666)
if err != nil {
log.Fatalf("open file error: %v", err)
}
// main退出之前,关闭文件
defer f.Close()
// 调用SetOutput设置日志输出的地方
log.SetOutput(f)
//log.SetOutput(io.MultiWriter(os.Stdout, f))
// 通过SetFlags设置Logger结构体里的flag属性
log.SetFlags(log.Ldate | log.Ltime | log.Lshortfile | log.Lmsgprefix)
// 通过SetPrefix设置Logger结构体里的prefix属性
log.SetPrefix("INFO:")
// 调用辅助函数Println打印日志到指定文件
log.Println("your message")
}
上面的代码会在当前目录下生成一个app_YYYYMMDD.log文件,log.Println里打印的内容会输出到这个文件里。细心的同学,可能看到了上面被注释的一行代码:
log.SetOutput(io.MultiWriter(os.Stdout, f))
这个表示的含义是同时把打印内容输出到标准输出(控制台)和指定文件里。
方式1只建议打印到控制台的时候使用,对于打印到日志文件的场景,建议使用自定义Logger,参考如下代码:
// example3.go
package main
import (
"fmt"
"log"
"os"
"time"
)
func main() {
// 打开文件
fileName := fmt.Sprintf("app_%s.log", time.Now().Format("20060102"))
f, err := os.OpenFile(fileName, os.O_RDWR | os.O_APPEND | os.O_CREATE, 0666)
if err != nil {
log.Fatalf("open file error: %v", err)
}
// 通过New方法自定义Logger,New的参数对应的是Logger结构体的output, prefix和flag字段
logger := log.New(f, "[INFO] ", log.LstdFlags | log.Lshortfile | log.Lmsgprefix)
// 调用Logger的方法Println打印日志到指定文件
logger.Println("your message")
}
上面的代码会在当前目录下生成一个app_YYYYMMDD.log文件,logger.Println里打印的内容会输出到这个文件里。
注意:New函数返回的是Logger指针,Logger结构体的方法都是指针接受者。
总结方式2的使用流程如下:
- 通过log.New创建一个新的Logger指针,在New函数里指定好output, prefix和flag等日志属性
- 调用log包里的辅助函数Print[f|ln],Fatal[f|ln],Panic[f|ln]打印日志
- Fatal[f|ln]打印日志后会调用os.Exit(1)
- Panic[f|ln]打印日志后会调用panic
自定义Logger的方式,还可以实现打印日志到控制台,也可以实现同时打印日志到日志文件和控制台,只需要给New函数的第一个参数传递对应的io.Writer类型参数即可。
- 如果要打印到控制台,参数可以用os.Stdout或者os.Stderr
- 如果要同时打印到控制台和日志文件,参数可以用io.MultiWriter(os.Stdout, f),参考上面的example2.go。
生产系统中打印日志就比上面的要复杂多了,需要考虑至少以下几个方面:
-
日志路径设置:支持配置日志文件路径,将日志打印到指定路径的文件里。
-
日志级别控制:支持Debug, Info, Warn, Error, Fatal等不同日志级别。
-
日志切割:可以按照日期和日志大小进行自动切割。
-
性能:在大量日志打印的时候不能对应用程序性能造成明显影响。
Go生态中,目前比较流行的是Uber开发的zap,在GitHub上的开源地址:https://github.com/uber-go/zap
- Lmsgprefix属性:不开启该属性时,Logger结构体里的prefix属性就会在每行日志最开头。开启该属性后,prefix就会在被打印的具体内容之前,而不是在每行最开头。
- LUTC属性:对于Logger结构体里的flag属性,如果开启了LUTC属性,那打印的日志里显示的时间就不是本地时间了,而是UTC标准时间。比如中国在东八区,中国时间减去8小时就是UTC时间。
- Fatal[f|ln]:打印日志后,会调用os.Exit(1)。如果defer关键字和Fatal[f|ln]一起使用要小心,因为如果在函数里执行了defer,但是最后是由于调用了os.Exit而退出的函数,那被defer的函数和方法是不会执行的。具体可以参考我之前写的文章Go语言里被defer的函数一定会执行么?
- Panic[f|ln]:打印日志后会调用panic,应用程序要考虑是否要通过recover来捕获panic,避免程序退出。
- log打印的日志一定会换行。所以即使调用的是log包里的Print函数或方法,打印的日志也会换行。因此使用log包里的Print和Println没有区别了。
代码开源地址:https://github.com/jincheng9/go-tutorial
也欢迎关注微信公众号:coding进阶,学习更多Go、微服务和云原生架构相关知识。