开发软件日志很重要,不管是应用的标准输出信息,还是主动打印的日志,还是被动的异常信息;都有被记录和整理的需求。大数据从哪里来,很多时候其实是从日志中来的。日志很重要,Go标准库也为日志输出内置了大量实用方法。

标准配置

标准库就提供了这些参数,按需要配置就好了:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
const (
	Ldate         = 1 << iota     // 日期示例:2021/01/19
	Ltime                         // 时间示例:15:57:48
	Lmicroseconds                 // 毫秒示例:15:58:13.694218
	Llongfile                     // 绝对路径和行号:/a/b/c.go:23
	Lshortfile                    // 文件和行号:c.go:23
	LUTC                          // 日期时间转为0时区
	Lmsgprefix                    // 取消打印前置字符串
	LstdFlags     = Ldate | Ltime // Go提供的标准抬头信息
)

日志前缀

可以自定义日志前缀,这样方便不同的子模块的日志混合在一起的情况下,也能很好的检索。

1
2
3
4
5
func init() {
    //log.SetFlags(log.Lmsgprefix) // 取消前置字符串
	log.SetPrefix("[GoFast]")    // 前置字符串加上特定标记
	log.SetFlags(log.LstdFlags)  // 设置成日期+时间 格式
}

下面就是效果示例:

[GoFast]2021/01/19 08:01:21 Listening and serving HTTP on 127.0.0.1:8099
[GoFast]2021/01/19 08:01:21 App OnReady Call.

特殊日志

log包除了有Print系列的函数,还有Fatal以及Panic系列的函数,其中Fatal表示程序遇到了致命的错误,需要退出,这时候使用Fatal记录日志后,然后程序退出,也就是说Fatal相当于先调用Print打印日志,然后再调用os.Exit(1)退出程序。

同理Panic系列的函数也一样,表示先使用Print记录日志,然后调用panic()函数抛出一个恐慌,这时候除非使用recover()函数,否则程序就会打印错误堆栈信息,然后程序终止。

看看源代码:

1
2
3
4
5
6
7
8
9
func Fatal(v ...interface{}) {
	std.Output(2, fmt.Sprint(v...))
	os.Exit(1)
}
func Panic(v ...interface{}) {
	s := fmt.Sprint(v...)
	std.Output(2, s)
	panic(s)
}

日志底层封装

看看主要的源代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
var (
	Stdin  = NewFile(uintptr(syscall.Stdin), "/dev/stdin")
	Stdout = NewFile(uintptr(syscall.Stdout), "/dev/stdout")
	Stderr = NewFile(uintptr(syscall.Stderr), "/dev/stderr")
)

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
}

func New(out io.Writer, prefix string, flag int) *Logger {
	return &Logger{out: out, prefix: prefix, flag: flag}
}

var std = New(os.Stderr, "", LstdFlags)

func Print(v ...interface{}) {
	std.Output(2, fmt.Sprint(v...))
}

源代码可以看出,变量std其实是一个*Logger,通过log.New函数创建,默认输出到os.Stderr设备,前缀为空,日志抬头信息为标准抬头LstdFlagsos.Stderr对应的是UNIX里的标准错误警告信息的输出设备,同时被作为默认的日志输出目的地。除此之外,还有标准输出设备os.Stdout以及标准输入设备os.Stdin

解释下这里的Logger类:

  1. 字段mu是一个互斥锁,主要是是保证这个日志记录器Logger是goroutine安全的。
  2. 字段prefix是每一行日志的前缀。
  3. 字段flag是日志抬头信息。
  4. 字段out是日志输出的目的地,默认情况下是os.Stderr
  5. 字段buf是一次日志输出文本缓冲,最终会被写到out里。

log包的SetOutput函数,可以设置输出目的地。这里稍微简单介绍下runtime.Caller,它可以获取运行时方法的调用信息。

func Caller(skip int) (pc uintptr, file string, line int, ok bool)

参数skip表示跳过栈帧数,0表示不跳过,也就是runtime.Caller的调用者。1的话就是再向上一层,表示调用者的调用者。log日志包里使用的是2,也就是表示我们在源代码中调用log.Printlog.Fatallog.Panic这些函数的调用者。

main函数调用log.Println为例,是main->log.Println->*Logger.Output->runtime.Caller这么一个方法调用栈,所以这时候,skip的值分别代表:

  1. 0表示*Logger.Output中调用runtime.Caller的源代码文件和行号
  2. 1表示log.Println中调用*Logger.Output的源代码文件和行号
  3. 2表示main中调用log.Println的源代码文件和行号

这也就是log包里的这个skip的值为什么一直是2的原因。

定制日志

标准库中的log包只是定义了一种通用的日志类型,输出都到了os.Stderr,你也可以定义自己的日志系统。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
var (
	Trace *log.Logger
	Info  *log.Logger
	Warn  *log.Logger
	Error *log.Logger
)

func init() {
	file, err := os.OpenFile("errors.txt", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)
	if err != nil {
		log.Fatalln("Failed to open error log file: ", err)
	}

	Trace = log.New(ioutil.Discard, "TRACE: ", log.Ldate|log.Ltime|log.Lshortfile)
	Info = log.New(os.Stdout, "INFO: ", log.Ldate|log.Ltime|log.Lshortfile)
	Warn = log.New(os.Stdout, "WARN: ", log.Ldate|log.Ltime|log.Lshortfile)

	Error = log.New(io.MultiWriter(file, os.Stderr), "ERROR: ",
                    log.Ldate|log.Ltime|log.Lshortfile)
}

func LoggerDemo() {
	Trace.Println("I have something standard to say.")
	Info.Println("Special Information")
	Warn.Println("There is something you need to know about")
	Error.Println("Something has failed")
}

控制台输出:

INFO: 2021/01/19 18:08:09 cus_logger.go:32: Special Information
WARN: 2021/01/19 18:08:09 cus_logger.go:33: There is something you need to know about
ERROR: 2021/01/19 18:08:09 cus_logger.go:34: Something has failed

还有你会发现在项目根目录新加了一个文件errors.txt,里面的内容是:

ERROR: 2021/01/19 18:08:09 cus_logger.go:34: Something has failed

小结:

这里我们通过定义多个Logger来区分不同的日志级别,使用比较麻烦;针对这种情况,可以使用第三方的log框架;也可以自己包装定义,直接通过不同级别的方法来记录不同级别的日志,还可以设置记录日志的级别等。

(完)