用 Go 从零实现日志包 - 第一篇 日志包初始化与输出级别

400 阅读5分钟

本文是用Go从零实现日志包教程系列的第一篇。

  • 实现日志包初始化,代码约 64 行
  • 介绍常见的输出级别:Info,Warning,Error 和 Fatal

1 日志包初始化

闭上眼睛,思考日志包最基本的功能,就是需要初始化的配置。日志是标准输出还是输出到文件?如果输出到文件,那么日志文件存在哪里呢?

新建 options.go 文件,我们创建一个结构体类型 Options,提供上面提到的日志配置。

type Options struct {
    out       io.Writer // 输出路径
    isDiscard bool      // 输出格式,其实从兼容性的角度考虑,应该使用 int32 类型标识
}

参考目前大部分日志包开源项目,我发现成熟的日志包,选项功能都很多,初始化参数复杂,这会影响调用方使用,所以我选择使用选项设计模式来初始化 options。选项设计模式的实现就是给一个结构体,定义一个函数,给结构体初始化。编辑 options.go 文件。

// 定义一个函数
type OptionFunc func(*Options)

// 给结构体初始化
func initOptions(opts ...OptionFunc) *Options {
	options := &Options{}
	for _, opt := range opts {
		opt(options)
	}
	if options.out == nil {
		options.out = os.Stderr
	}
	return options
}

// 修改配置
func WithOut(out io.Writer) OptionFunc {
	return func(options *Options) {
		options.out = out
	}
}

初始化日志的选项我们完成了,接下来就是日志包的构造函数,即根据日志配置 Options 创建 Logger,Logger 就是日志记录对象。调用者可以通过调 Logger 的方法打印日志。让我们创建一个新文件 logger.go。

type Logger struct {
	mu  sync.Mutex
	opt *Options
}

func New(opts *Options) *Logger {
	return &Logger{
		mu:  sync.Mutex{},
		opt: opts,
	}
}

var logger *Logger

func DefaultLogger(opts ...OptionFunc) {
	logger = New(initOptions(opts...))
}

在 Logger 中加了一把锁,是为了保证原子输入,保护 Logger 结构体中字段的写入。

提供 DefaultLogger 方法来构造一个默认的日志实例,为什么不直接使用 New 方法构造呢?DefaultLogger 方法不也是调用 New 吗?我们当然可以直接调用 New 来构造,不过我们通过 DefaultLogger 构造,是为了让调用者从方法名知道,通过这个方法能够构造默认的日志实例,不用自己去配置日志选项,未来我们可以提供更多不同配置的日志构造方法,例如:DefaultConsoleLogger 默认输出到控制台的日志实例,DefaultJsonLogger 输出 Json 格式的日志实例等。

现在我们日志的初始化时不完整的,还缺少了日志内容,即我们记录的日志应该有什么东西。 常见的日志内容有:

  • 日志级别
  • 格式化的时间
  • 打印了什么自定义信息
  • 哪个文件的哪一行打印的日志

新建文件 entry.go ,在 entry.go 中,将日志打印的内容抽象为结构体 entry,即内容条目,定义如下结构体:

type Entry struct {
	Line   int           // 文件中的哪行
	File   string        // 哪个文件打印的
	Body   []interface{} // 自定义信息
	Time   time.Time     // 时间
	Logger *Logger	     // logger 配置
	Buffer *bytes.Buffer // 写入缓冲区
}

func initEntry(logger *Logger) *Entry {
	return &Entry{Logger: logger, Buffer: new(bytes.Buffer)}
}

修改 logger.go 文件,将我们的 Entry 添加到 Logger 日志实例中。

type Logger struct {
	mu        sync.Mutex
	opt       *Options
	entryPool *sync.Pool
}

func New(opts *Options) *Logger {
	logger := &Logger{
		mu:  sync.Mutex{},
		opt: opts,
	}
	logger.entryPool = &sync.Pool{
		New: func() interface{} {
			return initEntry(logger)
		},
	}
	return logger
}

entry 条目我们使用对象池进行保存,因为日志的打印是频繁的!使用 *sync.Pool 保存和复用临时对象,减少内存分配,降低 GC 的压力,够极大地提升性能。(假如你还是不太理解为什么使用 *sync.Pool,别着急,我们完成日志打印函数后你就会明白了)。

我们已经完成了日志包的初始化,但是还没写日志打印方法,在写打印方法前,我们先了解一下日志的输出级别。

2 日志输出级别简介

编写打印日志方法前,先了解一下日志级别,是一种对日志数据进行分类的概念。分类后,将做到快速定位,排除了与目的不符的“噪音”。

常见的日志级别有 Fatal > Error > Warning > Info,级别越高,说明对程序的影响越大。下面分别介绍它们。

2.1 Fatal 

问题相当严重,程序遇到了致命错误,无法继续执行。例如数据库连接失败。 在 Go 的 log 标准库中,Fatal 日志级别的函数调用后,会调用 os.Exit ,这意味着:

  • 其他 goroutine defer 语句不会被执行;
  • 各种 buffers 不会被 flush,包括日志的;
  • 临时文件或目录不会被移除。

2.2 Error

程序执行出错了,这些错误会影响到程序的处理结果,这是最常用的错误级别(希望它不会是最常见的)。

假如我们现在一个函数 A 将返回 error,那么我们应该在调用该函数 A 的地方打印日志并进行降级处理,提供有损服务,即处理错误,而不是在被调用函数里打印日志。

Go 语言贡献者 Davio 认为,对错误进行降级处理后,应该打印 info 级别的日志,这意味着我处理了错误,或者往上抛并 warp 它。

2.3 Warning

警告,这类日志往往说明程序运行异常,但是又不影响程序继续运行。

在实际开发过程中,往往没人看警告,因为从定义上来讲,它并没有出现什么错误,也许是将来会出问题,按照 Go 语言的设计哲学,所有的警告都是错误,因此我们应该尽量消除它,别忽视它。我们可以结合 CICD 流程,强制 Warning 为 Error。

2.4 Info 

Info 级别,用来记录一些供运营分析的数据,没有什么好说的。