Golang中log日志包的使用

3,745 阅读2分钟

1.前言

作为后端开发人员,日志文件记录了发生在操作系统或其他软件运行时的事件或状态。技术人员可以通过日志记录进而判断系统的运行状态,寻找导致系统出错、崩溃的成因等。这是我们分析程序问题常用的手段。

2.log包介绍

在Golang中记录日志非常方便,Golang提供了一个简单的日志记录包log,包中定义了一个结构体类型 Logger,是整个包的基础部分,包中的其他方法都是围绕这整个结构体创建的。

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
}

该结构体表示一个日志对象,通过io.Writer进行日志输出,每次记录都简单地调用io.Writerwrite方法。一个Logger可以被多个goroutines同步执行。对各个成员含义解析:

mu :是sync.Mutex,它是一个同步互斥锁,用于保证日志记录的原子性.
prefix :是输入的日志每一行的前缀
flag :是一个标志,用于设置日志的打印格式
out :日志的输出目标,需要是一个实现了 io.Writer接口的对象,如: os.Stdout, os.Stderr, os.File等等
buf :用于缓存数据

flag可选值

其中flag的值在log包中定义了一些常量,它的作用主要是用于标识日志信息附加携带的信息:

// 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
)

如可以指定日期、时间、毫秒时间、绝对路径和行号、文件名和行号等,LstdFlags为默认的flag,只同时携带了日期和时间两个信息。

3.log包的使用

3.1 日志输出方法

log包中定义了如下的一套日志信息输出方法:

func (l *Logger) Print(v ...interface{}) //直接打印输出
func (l *Logger) Fatal(v ...interface{}) //输出日志后立即结束程序
func (l *Logger) Panic(v ...interface{}) //输出日志后抛出异常

和这三个方法相似的另外的方法都很好理解,就是换行或者格式化输出。

3.2 自定义创建日志对象

log包定义了一个New方法,并通过默认flag初始化了一个全局的私有logger:

// 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)

如果你刚好只需要日期和时间这两个额外的信息,就可以直接通过log包名调用方法,默认就是使用的这个初始化的std结构体的对应方法,如:

log.Println("hello,world~")

如果默认提供的日志配置不能满足您的需求,我们就可以自己去主动调用这个New方法,配置创建我们自己的logger。该方法所需的三个参数,描述如下:

out io.Writer:表示输出位置,可选值如 os.Stdout 为系统控制台,os.OpenFile 输出到单独的文件
prefix string: 表示统一前缀,会添加到生成的每一条日志前面,如debug的场景我们可以单独使用一个为[DEBUG]的前缀
flag int:表示额外信息标识,上文介绍过

当然,我们也可以单独调用如下相关的方法来单独设置。

func (l *Logger) SetOutput(w io.Writer)
func (l *Logger) SetFlags(flag int)
func (l *Logger) SetPrefix(prefix string)

3.3 封装自定义日志包

到这里,聪明的小伙伴一定有些思路根据自己的应用场景,基于官方的log包,封装出自己的log日志包了~ 如下提供一个示例,具体还请自行优化代码结构:

package logger

import (
	"io"
	"log"
	"os"
)

const (
	flag           = log.Ldate | log.Ltime | log.Lshortfile
	preDebug       = "[DEBUG]"
	preInfo        = "[INFO]"
	preWarning     = "[WARNING]"
	preError       = "[ERROR]"
)

var (
	logFile       io.Writer
	debugLogger   *log.Logger
	infoLogger    *log.Logger
	warningLogger *log.Logger
	errorLogger   *log.Logger
	defaultLogFile = "/var/log/web.log"
)

func init() {
	var err error
	logFile, err = os.OpenFile(defaultLogFile, os.O_CREATE|os.O_APPEND|os.O_RDWR, 0666)
	if err != nil {
		defaultLogFile = "./web.log"
		logFile, err = os.OpenFile(defaultLogFile, os.O_CREATE|os.O_APPEND|os.O_RDWR, 0666)
		if err != nil {
			log.Fatalf("create log file err %+v", err)
		}
	}
	debugLogger = log.New(logFile, preDebug, flag)
	infoLogger = log.New(logFile, preInfo, flag)
	warningLogger = log.New(logFile, preWarning, flag)
	errorLogger = log.New(logFile, preError, flag)
}

func Debugf(format string, v ...interface{}) {
	debugLogger.Printf(format, v...)
}

func Infof(format string, v ...interface{}) {
	infoLogger.Printf(format, v...)
}

func Warningf(format string, v ...interface{}) {
	warningLogger.Printf(format, v...)
}

func Errorf(format string, v ...interface{}) {
	errorLogger.Printf(format, v...)
}

func SetOutputPath(path string) {
	var err error
	logFile, err = os.OpenFile(path, os.O_CREATE|os.O_APPEND|os.O_RDWR, 0666)
	if err != nil {
		log.Fatalf("create log file err %+v", err)
	}
	debugLogger.SetOutput(logFile)
	infoLogger.SetOutput(logFile)
	warningLogger.SetOutput(logFile)
	errorLogger.SetOutput(logFile)
}
package main

import "yourPath/logger"

func main() {
	author := "korbin"
	logger.Debugf("hello,%s",author)
	logger.Infof("hello,%s",author)
	logger.Warningf("hello,%s",author)
	logger.Errorf("hello,%s",author)
}
[DEBUG]2020/12/01 11:33:07 logger.go:43: hello,korbin
[INFO]2020/12/01 11:33:07 logger.go:47: hello,korbin
[WARNING]2020/12/01 11:33:07 logger.go:51: hello,korbin
[ERROR]2020/12/01 11:33:07 logger.go:55: hello,korbin

如该示例中的输出文件路径可以改为通过从配置文件读取或者通过命令行参数等,或者输出方式改为os.Stderr控制台等都行,根据自行情况而定。

3.4 log包进一步解析

另外其实我们也很容易发现,Logger结构体的日志输出方法,都是通过调用func (l *Logger) Output(calldepth int, s string) error方法实现的,在 Output 方法中,做了如下这些事情:

  1. 获取当前事件
  2. 对 Logger实例进行加锁操作
  3. 判断Logger的标志位是否包含 Lshortfile 或 Llongfile, 如果包含进入步骤4, 如果不包含进入步骤5
  4. 获取当前函数调用所在的文件和行号信息
  5. 格式化数据,并将数据写入到 l.out 中,完成输出
  6. 解锁操作

log包整体结构还是很简单的,有兴趣的小伙伴可以再自己多看一下源码。