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系列 之外,还有 FatalPanic 系列,与 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...))。这里涉及到 stdOutput()

考察 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.logwarning.logerror.log 被生成,不同文件中被写入了不同的信息。

参考

Package log
Go语言实战笔记(十八)| Go log 日志

Add a Comment

您的邮箱地址不会被公开。 必填项已用 * 标注

Close Bitnami banner