ITPub博客

首页 > > > Go 每日一库之 log

Go 每日一库之 log

作者:darjun 时间:2020-04-26 14:36:37 0 删除 编辑

简介

在日常开发中,日志是必不可少的功能。虽然有时可以用fmt库输出一些信息,但是灵活性不够。Go 标准库提供了一个日志库log。本文介绍log库的使用。

快速使用

log是 Go 标准库提供的,不需要另外安装。可直接使用:

package main
import ( "log")
type User struct { Name string Age int}
func main() { u := User{ Name: "dj", Age: 18, }
log.Printf("%s login, age:%d", u.Name, u.Age) log.Panicf("Oh, system error when %s login", u.Name) log.Fatalf("Danger! hacker %s login", u.Name)}

log默认输出到标准错误(stderr),每条日志前会自动加上日期和时间。如果日志不是以换行符结尾的,那么log会自动加上换行符。即每条日志会在新行中输出。

log提供了三组函数:

•Print/Printf/Println:正常输出日志;•Panic/Panicf/Panicln:输出日志后,以拼装好的字符串为参数调用panic;•Fatal/Fatalf/Fatalln:输出日志后,调用os.Exit(1)退出程序。

命名比较容易辨别,带f后缀的有格式化功能,带ln后缀的会在日志后增加一个换行符。

注意,上面的程序中由于调用log.Panicf会panic,所以log.Fatalf并不会调用。

定制

前缀

调用log.SetPrefix为每条日志文本前增加一个前缀。例如,在上面的程序中设置Login:前缀:

package main
import ( "log")
type User struct { Name string Age int}
func main() { u := User{ Name: "dj", Age: 18, }
log.SetPrefix("Login: ") log.Printf("%s login, age:%d", u.Name, u.Age)}

调用log.Prefix可以获取当前设置的前缀。

选项

设置选项可在每条输出的文本前增加一些额外信息,如日期时间、文件名等。

log库提供了 6 个选项:

// src/log/log.goconst (  Ldate         = 1 << iota  Ltime                      Lmicroseconds              Llongfile                  Lshortfile                 LUTC                     )

•Ldate:输出当地时区的日期,如2020/02/07;•Ltime:输出当地时区的时间,如11:45:45;•Lmicroseconds:输出的时间精确到微秒,设置了该选项就不用设置Ltime了。如11:45:45.123123;•Llongfile:输出长文件名+行号,含包名,如github.com/darjun/go-daily-lib/log/flag/main.go:50;•Lshortfile:输出短文件名+行号,不含包名,如main.go:50;•LUTC:如果设置了Ldate或Ltime,将输出 UTC 时间,而非当地时区。

调用log.SetFlag设置选项,可以一次设置多个:

package main
import ( "log")
type User struct { Name string Age int}
func main() { u := User{ Name: "dj", Age: 18, }
log.SetFlags(log.Lshortfile | log.Ldate | log.Lmicroseconds)
log.Printf("%s login, age:%d", u.Name, u.Age)}

调用log.Flags()可以获取当前设置的选项。

运行代码,输出:

2020/02/07 11:56:59.061615 main.go:20: dj login, age:18

注意,调用log.SetFlag之后,原有的选项会被覆盖掉!

log库还定义了一个Lstdflag,为Ldate | Ltime,这就是我们默认的选项。

// src/log/log.goconst (  LstdFlags = Ldate | Ltime)

这就是为什么默认情况下,每条日志前会自动加上日期和时间。

自定义

实际上,log库为我们定义了一个默认的Logger,名为std,意为标准日志。我们直接调用的log库的方法,其内部是调用std的对应方法:

// src/log/log.govar std = New(os.Stderr, "", LstdFlags)
func Printf(format string, v ...interface{}) { std.Output(2, fmt.Sprintf(format, v...))}
func Fatalf(format string, v ...interface{}) { std.Output(2, fmt.Sprintf(format, v...)) os.Exit(1)}
func Panicf(format string, v ...interface{}) { s := fmt.Sprintf(format, v...) std.Output(2, s) panic(s)}

当然,我们也可以定义自己的Logger:

package main
import ( "bytes" "fmt" "log")
type User struct { Name string Age int}
func main() { u := User{ Name: "dj", Age: 18, }
buf := &bytes.Buffer{} logger := log.New(buf, "", log.Lshortfile|log.LstdFlags)
logger.Printf("%s login, age:%d", u.Name, u.Age)
fmt.Print(buf.String())}

log.New接受三个参数:

•io.Writer:日志都会写到这个Writer中;•prefix:前缀,也可以后面调用logger.SetPrefix设置;•flag:选项,也可以后面调用logger.SetFlag设置。

上面代码将日志输出到一个bytes.Buffer,然后将这个buf打印到标准输出。

运行代码:

$ go run main.go 2020/02/07 13:48:54 main.go:23: dj login, age:18

注意到,第一个参数为io.Writer,我们可以使用io.MultiWriter实现多目的地输出。下面我们将日志同时输出到标准输出、bytes.Buffer和文件中:

package main
import ( "bytes" "io" "log" "os")
type User struct { Name string Age int}
func main() { u := User{ Name: "dj", Age: 18, }
writer1 := &bytes.Buffer{} writer2 := os.Stdout writer3, err := os.OpenFile("log.txt", os.O_WRONLY|os.O_CREATE, 0755) if err != nil { log.Fatalf("create file log.txt failed: %v", err) }
logger := log.New(io.MultiWriter(writer1, writer2, writer3), "", log.Lshortfile|log.LstdFlags) logger.Printf("%s login, age:%d", u.Name, u.Age)}

如果你愿意,还可以发送到到网络。

实现

log库的核心是Output方法,我们简单看一下:

// src/log/log.gofunc (l *Logger) Output(calldepth int, s string) error {  now := time.Now() // get this early.  var file string  var line int  l.mu.Lock()  defer l.mu.Unlock()  if l.flag&(Lshortfile|Llongfile) !=  {    // Release lock while getting caller info - it's expensive.    l.mu.Unlock()    var ok bool    _, file, line, ok = runtime.Caller(calldepth)    if !ok {      file = "???"      line =     }    l.mu.Lock()  }  l.buf = l.buf[:]  l.formatHeader(&l.buf, now, file, line)  l.buf = append(l.buf, s...)  if len(s) ==  || s[len(s)-1] != '\n' {    l.buf = append(l.buf, '\n')  }  _, err := l.out.Write(l.buf)  return err}

如果设置了Lshortfile或Llongfile,Ouput方法中会调用runtime.Caller获取文件名和行号。runtime.Caller的参数calldepth表示获取调用栈向上多少层的信息,当前层为 0。

一般的调用路径是:

•程序中使用log.Printf之类的函数;•在log.Printf内调用std.Output。

我们在Output方法中需要获取调用log.Printf的文件和行号。 calldepth传入 0 表示Output方法内调用runtime.Caller的那一行信息,传入 1 表示log.Printf内调用std.Output那一行的信息, 传入 2 表示程序中调用log.Printf的那一行信息。显然这里要用 2。

然后调用formatHeader处理前缀和选项。

最后将生成的字节流写入到Writer中。

这里有两个优化技巧:

•由于runtime.Caller调用比较耗时,先释放锁,避免等待时间过长;•为了避免频繁的内存分配,logger中保存了一个类型为[]byte的buf,可重复使用。前缀和日志内容先写到这个buf中,然后统一写入Writer,减少 io 操作。

总结

log实现了一个小巧的日志库,可供简单使用。本文介绍了它的基本使用,简单地分析了一下源码。如果log库的功能不能满足需求,我们可以在它之上做二次封装。看煎鱼大佬的这篇文章。

除此之外,社区也涌现了很多优秀的、功能丰富的日志库,可以选用。

参考

1.log官方文档

我

我的博客

欢迎关注我的微信公众号【GoUpUp】,共同学习,一起进步~



来自 “ ITPUB博客 ” ,链接:https://blog.itpub.net/69972593/viewspace-3144764/ ,如需转载,请注明出处,否则将追究法律责任。

请登录后发表评论 登录
全部评论

注册时间:2020-04-24

  • 博文量
    46
  • 访问量
    0