Go语言log包的使用介绍
基本使用
通过fmt包获得的信息量较少,所以我们可以通过log包来获取更多相关信息,例如:日期,时间,文件名等等。
fmt.Print("hello world")
fmt.Println("hello world")
fmt.Printf("print: %s\n","hello world")
//结果
//hello worldhello world
//print: hello world
log.Println("hello world")
log.Printf("print: %s\n","hello world")
log.Print("hello world")
//结果
//2020/04/14 15:45:46 hello world
//2020/04/14 15:45:46 print: hello world
//2020/04/14 15:45:46 hello world
默认情况下使用log包时,会比fmt包多输出了 日期
和 时间
抬头。注意:输出内容不带有换行符,则会自动换行;如果带有换行符,则不会被再次换行,来保证每一个输出为独立的一行。
如果想要输出更多的相关信息,则需要对输出的抬头进行设置。
//设置输出的抬头为:日期,时间,文件和行号
log.SetFlags(log.Ldate|log.Ltime |log.Lshortfile)
log.Printf("print: %s\n","hello world")
//结果
//2020/04/14 15:53:15 main.go:47: print: hello world
可以设置的选项为:
const (
Ldate = 1 << iota //日期示例: 2009/01/23
Ltime //时间示例: 01:23:23
Lmicroseconds //毫秒示例: 01:23:23.123123.
Llongfile //绝对路径和行号: /a/b/c/d.go:23
Lshortfile //文件和行号: d.go:23.
LUTC //日期时间转为0时区的
LstdFlags = Ldate | Ltime //Go提供的标准抬头信息
)
默认的情况下设置为 LstdFlags
。
通过 log.SetPrefix()
还可以设置输出内容的前缀。
log.SetPrefix("[test]")
log.Printf("print: %s\n","hello world")
//结果
//[test]2020/04/14 15:59:57 print: hello world
log包中除了 Print系列
之外,还有 Fatal
和 Panic
系列,与 Print系列
基本用法一致,可以参考官方文档,这里就不再赘述了。
源码分析
我们用 log.Printf()
来作一个分析。
//log.Printf()源码
func Printf(format string, v ...interface{}) {
std.Output(2, fmt.Sprintf(format, v...))
}
调用Printf()
后返回 std.Output(2, fmt.Sprintf(format, v...))
。这里涉及到 std
和 Output()
。
考察 std
首先来考察一下 std
。
//通过调用New(), 构造一个std结构体
var std = New(os.Stderr, "", LstdFlags)
//std的构造函数
func New(out io.Writer, prefix string, flag int) *Logger {
return &Logger{out: out, prefix: prefix, flag: flag}
}
由于 std
是一个全局变量,因此当我们调用 log包
时,New()
会事先被执行,构造一个 Logger
,并将地址赋值给 std
。
Logger
log包中的唯一一个结构体,也是整个包的核心,具体结构如下:
type Logger struct {
mu sync.Mutex // ensures atomic writes; protects the following fields
prefix string // prefix to write at beginning of each line
flag int // properties
out io.Writer // destination for output
buf []byte // for accumulating text to write
}
字段mu是一个互斥锁,主要是是保证这个日志记录器Logger在多goroutine下也是安全的。
字段prefix是每一行日志的前缀。
字段flag是日志抬头信息。
字段out是日志输出的目的地,默认情况下是os.Stderr。
字段buf是一次日志输出文本缓冲,最终会被写到out里。
回到 std
中被构造的 Logger
,
Logger.out
的值为 os.Stderr
,即UNIX里的标准错误警告信息;
Logger.prefix
的值为空;
Logger.flag
的值为 LstdFlags
,也就是默认情下况输出抬头 日期
和 时间
;
其余都为零值。
考察 Output()
接着再来看一下 Output()
。
func (l *Logger) Output(calldepth int, s string) error {
//获取当前时间
now := time.Now() // get this early.
var file string
var line int
//加锁,保证多goroutine下的安全
l.mu.Lock()
defer l.mu.Unlock()
//如果配置了获取文件和行号的话
if l.flag&(Lshortfile|Llongfile) != 0 {
// Release lock while getting caller info - it's expensive.
l.mu.Unlock()
var ok bool
//因为runtime.Caller代价比较大,先不加锁
_, 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')
}
//输出拼接好的缓冲buf里的日志信息到目的地
_, err := l.out.Write(l.buf)
return err
}
Output()
是结构体 Logger
的一个重要方法,作用就是将结构体 Logger
中的内容格式化并输出到指定的位置。
func Printf(format string, v ...interface{}) {
std.Output(2, fmt.Sprintf(format, v...))
}
在 log.Printf()
的情况下,参数 calldepth
的值为 2,参数 s
的值为给定的字符串。 calldepth
的值设为 2 是为了找到 log.Printf()
函数的调用者,来确定其位置。
定制日志
假设我们有这样一些需求:
输出位置
在开发环境中将日志输出到控制台;
在测试环境中将日志输出到控制台和文件中;
在生产环境中将日志输出到文件中;
其余的情况都按照输出到控制台处理。
package main
import (
"io"
"log"
"os"
)
//根据环境来设置
//开发环境 => dev
//测试环境 => test
//生产环境 => prod
var Mode string = "prod"
var Logger *log.Logger
func init(){
switch Mode {
case "dev":
Logger = log.New(os.Stderr, "", log.LstdFlags )
case "test":
errFile,err:=os.OpenFile("errors.log",os.O_CREATE|os.O_WRONLY|os.O_APPEND,0744)
if err!=nil{
log.Fatalln("打开日志文件失败:",err)
}
Logger = log.New(io.MultiWriter(os.Stderr,errFile), "", log.LstdFlags)
case "prod" :
errFile,err:=os.OpenFile("errors.log",os.O_CREATE|os.O_WRONLY|os.O_APPEND,0744)
if err!=nil{
log.Fatalln("打开日志文件失败:",err)
}
Logger = log.New(errFile, "", log.LstdFlags)
default:
Logger = log.New(os.Stderr, "", log.LstdFlags )
}
}
func main() {
Logger.Println("error")
}
事先设置全局变量 Mode
以及 Logger
,为了允许外部调用,我们将变量首字母大写。根据 Mode
变量,通过 init()
的值来初始化 Logger
,达到不同的环境下输出日志到不同的位置。
输出内容
开发环境下输出标准抬头;
测试环境下输出日期,时间,绝对路径和行号;
生产环境下输出日期,时间,毫秒和文件名和行号,日期和时间为时间世界标准时间,并且增加前缀“【production】”。
func init(){
switch Mode {
case "dev":
//标准抬头
Logger = log.New(os.Stderr, "", log.LstdFlags )
case "test":
...
//输出日期,时间,绝对路径和行号
Logger = log.New(io.MultiWriter(os.Stderr,errFile), "", log.Ldate|log.Ltime|log.Llongfile)
case "prod" :
...
//输出日期,时间,毫秒和文件名和行号,日期和时间为时间世界标准时间,并且增加前缀“【production】”
Logger = log.New(errFile, "【production】", log.Ldate|log.Ltime|log.Lshortfile|log.LUTC)
default:
Logger = log.New(os.Stderr, "", log.LstdFlags )
}
}
这个需求并不困难,只在构造Logger时给与不同的参数即可。
输出级别
将输出级别分为:信息级(info),警告级(warning),错误级(error),生产环境下不同级别的内容输出到不同的文件中。
我们来分析一下这个需求,三个级别输出到不同位置的文件中去。而一个 Logger
的输出位置将已经在初始化时就确定了,虽然我们可以在输出前改变输出位置,但这样的效率就十分低下了。
此时这一个结构体 Logger
已经不能满足需求,因此我们需要构造三个不同的 Logger
来解决这个问题。
package main
import (
"io"
"log"
"os"
)
//生产环境 => prod
var Mode string = "prod"
var Info *log.Logger
var Warning *log.Logger
var Error *log.Logger
func init() {
//创建三个文件
infoFile, err := os.OpenFile("info.log", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0744)
if err != nil {
log.Fatalln("打开日志文件失败:", err)
}
warningFile, err := os.OpenFile("warning.log", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0744)
if err != nil {
log.Fatalln("打开日志文件失败:", err)
}
errorFile, err := os.OpenFile("error.log", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0744)
if err != nil {
log.Fatalln("打开日志文件失败:", err)
}
switch Mode {
case "dev":
Info = log.New(os.Stderr, "", log.LstdFlags)
Warning = log.New(os.Stderr, "", log.LstdFlags)
Error = log.New(os.Stderr, "", log.LstdFlags)
case "test":
Info = log.New(io.MultiWriter(os.Stderr, infoFile), "", log.Ldate|log.Ltime|log.Llongfile)
Warning = log.New(io.MultiWriter(os.Stderr, warningFile), "", log.Ldate|log.Ltime|log.Llongfile)
Error = log.New(io.MultiWriter(os.Stderr, errorFile), "", log.Ldate|log.Ltime|log.Llongfile)
case "prod":
Info = log.New(infoFile, "【production】", log.Ldate|log.Ltime|log.Lshortfile|log.LUTC)
Warning = log.New(warningFile, "【production】", log.Ldate|log.Ltime|log.Lshortfile|log.LUTC)
Error = log.New(errorFile, "【production】", log.Ldate|log.Ltime|log.Lshortfile|log.LUTC)
default:
Info = log.New(os.Stderr, "", log.LstdFlags)
Warning = log.New(os.Stderr, "", log.LstdFlags)
Error = log.New(os.Stderr, "", log.LstdFlags)
}
}
func main() {
Info.Println("info")
Warning.Println("warning")
Error.Println("error")
}
将Mode设置为 prod
,运行后可以看到 info.log
,warning.log
, error.log
被生成,不同文件中被写入了不同的信息。