Skip to content

Files

Latest commit

Oct 26, 2022
d81d36e · Oct 26, 2022

History

History

01

Folders and files

NameName
Last commit message
Last commit date

parent directory

..
Dec 2, 2021
Dec 2, 2021
Dec 2, 2021
Dec 2, 2021
Oct 26, 2022

Go标准库之log使用详解

简介

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使用方法

要使用log包打印日志,有2种方式,可以根据各自业务场景选择对应方法:

  • 方法1:使用log包里自带的std这个Logger指针。通常用于在控制台输出日志。
  • 方法2:自定义Logger。通常用于把日志输出到文件里。

方法1和方法2相比,没有本质区别,只是使用场景上有一个偏好。

当然方法1也可以实现输出日志到文件里,方法2也可以实现在控制台打印日志。

下面详细介绍下这两种方式的用法。

方式1:log自带的标准错误输出

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的使用流程如下:

  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的默认设置。

  2. 调用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))

这个表示的含义是同时把打印内容输出到标准输出(控制台)和指定文件里。

方式2:自定义Logger

方式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的使用流程如下:

  1. 通过log.New创建一个新的Logger指针,在New函数里指定好output, prefix和flag等日志属性
  2. 调用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、微服务和云原生架构相关知识。

References