log

Kesa...大约 5 分钟golanggolang daily lib

Introduction 简介

在日常开发中,日志功能是必不可少的,虽然可以使用fmt库输出一些信息,但灵活性不够;golang提供了标准库log,可以实现基本的日志功能

Quick Start 快速开始

func main() {
	user := User{
		ID:       1,
		Username: "Kesa",
	}

	log.Printf("User: %s login,ID:%d", user.Username, user.ID)
	log.Fatalf("Warning hacker %s detected", user.Username)

	// do not execute
	log.Panicf("User:%s[ID:%d] login failed", user.Username, user.ID)

}
$  go run ./main.go
2021/10/27 11:59:15 User: Kesa login,ID:1
2021/10/27 11:59:15 Warning hacker Kesa detected
exit status 1

log提供了三组函数:

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

log默认输出至标准错误(stderr),每条日志之前会加上日期和时间;若日志信息不是以换行符结尾的则会加上换行符,即每条日志均会在新行中打印

虽然日志均会在新行打印,但是PrintPrintln是有区别的,看下面的示例:

log.Print("A", 1, 2, "B")
log.Println("A", 1, 2, "B")
2021/10/27 14:17:38 A1 2B
2021/10/27 14:17:38 A 1 2 B

Print只会在两个均不为string型的参数间添加空格,如A1 2B;而Println会在所有的参数之间加空格,如A 1 2 B

定制

前缀

log.SetPrefix会为每条日志文本前添加一个前缀

type User struct {
	ID       int
	Username string
}

func main() {
	user := User{
		ID:       1,
		Username: "Kesa",
	}

	// set prefix
	log.SetPrefix("[Login]")

	log.Printf("User: %s login,ID:%d", user.Username, user.ID)
}
[Login]2021/10/27 14:25:08 User: Kesa login,ID:1
[Login]2021/10/27 14:25:08 Warning hacker Kesa detected

选项

设置选项可以在每条输出的文本前添加额外信息,如时间,文件名等

log提供了6个选项:

// src/log/log.go
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
)
  • Ldate:输出当地时区的日期,如:2021/10/27
  • Ltime:输出当地时区的时间,如:14:31:21
  • Lmicroseconds:输出时间精确到微秒,设置此选项就无需设置Ltime,如11:45:45.123123
  • Llongfile:输出长文件名和行号,含包名,如:/a/b/c/d.go:23
  • Lshortfile:输出短文件名和行号,不含包名,如:d.go:23
  • LUTC:若在LdateLtime之后设置,将输出UTC时间
  • Lmsgprefix:将设置的日志前缀从行首移动至日志信息的开头
  • LstdFlags:logger的默认设置,设置LdateLtime

使用log.SetFlags设置选项,可以设置多个:

func main() {
	user := User{
		ID:       1,
		Username: "Kesa",
	}

	// set prefix
	log.SetPrefix("[Login]")

	// set flag
	log.SetFlags(log.Llongfile | log.Ldate | log.Ltime | log.Lmsgprefix)

	log.Printf("User: %s login,ID:%d", user.Username, user.ID)
}
2021/10/27 16:14:17 main.go:22: [Login]User: Kesa login,ID:1

可以看到前缀[login]被移动到日志信息之前了

调用log.Flags()可以查看当前的设置,由于设置返回的值为整型,不够直观,可以编写函数转成字符串,在上述例子中添加以下代码:

func main(){
    // ...
    fmt.Println(LogFlagsToString(log.Flags()))
    // ...
}
func LogFlagsToString(flags int) string {
	var buffer bytes.Buffer

	if flags&log.Ldate == log.Ldate {
		buffer.WriteString("|Ldate")
	}
	if flags&log.Ltime == log.Ltime {
		buffer.WriteString("|Ltime")
	}
	if flags&log.Lmicroseconds == log.Lmicroseconds {
		buffer.WriteString("|Lmicroseconds")
	}
	if flags&log.Llongfile == log.Llongfile {
		buffer.WriteString("|Llongfile")
	}
	if flags&log.Lshortfile == log.Lshortfile {
		buffer.WriteString("|Lshortfile")
	}
	if flags&log.LUTC == log.LUTC {
		buffer.WriteString("|Ldate")
	}
	if flags&log.Lmsgprefix == log.Lmsgprefix {
		buffer.WriteString("|Lmsgprefix")
	}
	if flags&log.LstdFlags == log.LstdFlags {
		buffer.WriteString("|LstdFlags")
	}

	if buffer.Len() <= 0 {
		return ""
	}
	return buffer.String()[1:]
}

自定义

实际上,log库定义了一个默认的Logger std,意为标准日志,直接调用的log库的函数实际上内部会调用std的方法

// src/log/log.go
var 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:

// create-logger/main.go
type User struct {
	ID       int
	Username string
}

func main() {
	u := User{
		ID:       1,
		Username: "Kesa",
	}
	buf := &bytes.Buffer{}
	logger := log.New(buf, "[login]", log.LstdFlags|log.Lmsgprefix)
	logger.Printf("User %s(ID:%d) login ", u.Username, u.ID)
	fmt.Print(buf.String())
}
2021/10/27 17:21:55 [login]User Kesa(ID:1) login 

log.New接受三个参数:

  • io.Writer:日志信息会写入其中
  • prefix:日志前缀
  • flag:日志选项

上面的示例中将日志写入bytes.Buffer中,之后将buf打印到标准输出

log.New的第一个参数可以使用io.MultiWriter实现多目的输出,下例将日志同时输出至标准输出、buffer和文件中

type User struct {
	ID       int
	Username string
}

func main() {
	u := User{
		ID:       1,
		Username: "Kesa",
	}

	writer1 := &bytes.Buffer{}
	writer2 := os.Stdout
	writer3, err := os.OpenFile("./multi.log", os.O_WRONLY|os.O_CREATE, 0755)
	if err != nil {
		log.Fatal("create file failed:", err)
	}
	logger := log.New(io.MultiWriter(writer1, writer2, writer3), "[Multi]", log.LstdFlags|log.Lmsgprefix)
	logger.Printf("%s login,ID:%d", u.Username, u.ID)
}
go run ./main.go 
2021/10/27 20:49:35 [Multi] Kesa login,ID:1
Buf:2021/10/27 20:49:35 [Multi] Kesa login,ID:1

同时生成文件multi.log:

2021/10/27 20:52:32 [Multi]Kesa login,ID:1

实现

log库的核心是Output方法:

// src/log/log.go
func (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) != 0 {
    // 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 = 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')
  }
  _, err := l.out.Write(l.buf)
  return err
}

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

一般的调用路径为:

  • 程序中使用log.Printf之类的函数
  • log.Printf内调用std.Output
  • Output方法中需要获取调用log.Printf的文件和行号,这里calldepth为2
    • calldepth为0表示Output调用runtime.Caller的那一行信息
    • calldepth为1表示log.Printf调用Output的那一行信息
    • calldepth为2表示调用log.Printf的那一行信息
  • 调用formatHeader处理前缀和选项
  • 将生成的字节流写入Writer之中

值得注意的是此处有两个优化技巧:

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

Conclusion

标准库的log比较小巧,可以简单使用;但若其不满足功能,也有很多优秀的开源log库可供选择,如:zap,logrus,zerolog等

Reference 参考

  1. logopen in new window godoc
  2. logopen in new window darjun/blog
  3. log.Print() ? log.Println() ? What the difference ?open in new window google groups
  4. log.Print behaves like log.Printlnopen in new window golangopen in new window/goopen in new window issue
上次编辑于:
评论
  • 按正序
  • 按倒序
  • 按热度
Powered by Waline v2.15.2