这个示例程序展示如何使用最基本的 log 包。
// 这个示例程序展示如何使用最基本的log包
package main
import (
"log"
)
func init() {
log.SetPrefix("TRACE: ")
log.SetFlags(log.Ldate | log.Lmicroseconds | log.Llongfile)
}
func main() {
// Println写到标准日志记录器
log.Println("message")
// Fatalln在调用Println()之后会接着调用os.Exit(1)
log.Fatalln("fatal message")
// Panicln在调用Println()之后会接着调用panic()
log.Panicln("panic message")
}
输出:
TRACE: 2019/10/31 19:38:54.732475 /home/wohu/GoCode/src/hello.go:15: message
TRACE: 2019/10/31 19:38:54.732590 /home/wohu/GoCode/src/hello.go:18: fatal message
exit status 1
init
这个函数会在运行 main()
之前作为程序初始化的一部分执行。通常程序会在这个 init()
函数里配置日志参数,这样程序一开始就能使用 log 包进行正确的输出。
const (
// 将下面的位使用或运算符连接在一起,可以控制要输出的信息。没有
// 办法控制这些信息出现的顺序(下面会给出顺序)或者打印的格式
// (格式在注释里描述)。这些项后面会有一个冒号:
// 2009/01/23 01:23:23.123123 /a/b/c/d.go:23: message
// 日期: 2009/01/23
Ldate = 1 << iota
// 时间: 01:23:23
Ltime
// 毫秒级时间: 01:23:23.123123。该设置会覆盖Ltime标志
Lmicroseconds
// 完整路径的文件名和行号: /a/b/c/d.go:23
Llongfile
// 最终的文件名元素和行号: d.go:23
// 覆盖 Llongfile
Lshortfile
// 标准日志记录器的初始值
LstdFlags = Ldate | Ltime
)
这些标志用来控制可以写到每个日志项的其他信息。这些标志被声明为常量。
// 日期: 2009/01/23
Ldate = 1 << iota
关键字 iota
在常量声明区里有特殊的作用。这个关键字让编译器为每个常量复制相同的表达式,直到声明区结束,或者遇到一个新的赋值语句。
关键字 iota
的另一个功能是, iota
的初始值为 0,之后 iota
的值在每次处理为常量后,都会自增 1。
const (
Ldate = 1 << iota // 1 << 0 = 000000001 = 1
Ltime // 1 << 1 = 000000010 = 2
Lmicroseconds // 1 << 2 = 000000100 = 4
Llongfile // 1 << 3 = 000001000 = 8
Lshortfile // 1 << 4 = 000010000 = 16
...
)
操作符 <<
对左边的操作数执行按位左移操作。在每个常量声明时,都将 1 按位左移 iota
个位置。最终的效果使为每个常量赋予一个独立位置的位,这正好是标志希望的工作方式。
常量 LstdFlags
展示了如何使用这些标志,
const (
...
LstdFlags = Ldate(1) | Ltime(2) = 00000011 = 3
)
因为使用了复制操作符, LstdFlags
打破了 iota 常数链。由于有 |
运算符用于执行或操作,常量 LstdFlags
被赋值为 3。
对位进行或操作等同于将每个位置的位组合在一起,作为最终的值。如果对位 1 和 2 进行或操作,最终的结果就是3。
func init() {
...
log.SetFlags(log.Ldate | log.Lmicroseconds | log.Llongfile)
}
这里我们将 Ldate
、 Lmicroseconds
和 Llongfile
标志组合在一起,将该操作的值传入 SetFlags
函数。这些标志值组合在一起后,最终的值是 13,代表第 1、3 和 4 位为 1(00001101)。
由于每个常量表示单独一个位,这些标志经过或操作组合后的值,可以表示每个需要的日志参数。之后 log 包会按位检查这个传入的整数值,按照需求设置日志项记录的信息。
初始完 log 包后,可以看一下 main()
函数.
func main() {
// Println写到标准日志记录器
log.Println("message")
// Fatalln在调用Println()之后会接着调用os.Exit(1)
log.Fatalln("fatal message")
// Panicln在调用Println()之后会接着调用panic()
log.Panicln("panic message")
}
上述代码展示了如何使用 3 个函数 Println
、 Fatalln
和 Panicln
来写日志消息。这些函数也有可以格式化消息的版本,只需要用 f 替换结尾的 ln。
Fatal
系列函数用来写日志消息,然后使用 os.Exit(1)
终止程序。Panic
系列函数用来写日志消息,然后触发一个 panic
。
除非程序执行 recover
函数,否则会导致程序打印调用栈后终止。 Print
系列函数是写日志消息的标准方法。
log
包有一个很方便的地方就是,这些日志记录器是多 goroutine
安全的。这意味着在多个 goroutine
可以同时调用来自同一个日志记录器的这些函数,而不会有彼此间的写冲突。
要想创建一个定制的日志记录器,需要创建一个 Logger
类型值。可以给每个日志记录器配置一个单独的目的地,并独立设置其前缀和标志。
让我们来看一个示例程序,这个示例程序展示了如何创建不同的 Logger
类型的指针变量来支持不同的日志等级。
// 这个示例程序展示如何创建定制的日志记录器
package main
import (
"io"
"io/ioutil"
"log"
"os"
)
var ( // 为4个日志等级声明了4个Logger类型的指针变量
Trace *log.Logger // 记录所有日志
Info *log.Logger // 重要的信息
Warning *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, // 当某个等级的日志不重要时,使用Discard变量可以禁用这个等级的日志。
"TRACE: ",
log.Ldate|log.Ltime|log.Lshortfile)
Info = log.New(os.Stdout,
"INFO: ",
log.Ldate|log.Ltime|log.Lshortfile)
Warning = log.New(io.MultiWriter(file, os.Stdout),
"WARN: ",
log.Ldate|log.Ltime|log.Lshortfile)
// io.MultiWriter(file, os.Stderr)
/*
这个函数调用会返回一个io.Writer接口类型值,这个值包含之前打开的文件file,以及stderr。
MultiWriter函数是一个变参函数,可以接受任意个实现了io.Writer接口的值。
这个函数会返回一个io.Writer值,这个值会把所有传入的io.Writer的值绑在一起。
当对这个返回值进行写入时,会向所有绑在一起的io.Writer值做写入。
这让类似log.New这样的函数可以同时向多个Writer做输出。
现在,当我们使用Error记录器记录日志时,输出会同时写到文件和stderr。
*/
Error = log.New(io.MultiWriter(file, os.Stderr),
"ERROR: ",
log.Ldate|log.Ltime|log.Lshortfile)
}
func main() {
Trace.Println("I have something standard to say")
Info.Println("Special Information")
Warning.Println("There is something you need to know about")
Error.Println("Something has failed")
}
使用了 log
包的 New
函数,它创建并正确初始化一个 Logger
类型的值。函数 New
会返回新创建的值的地址。在 New
函数创建对应值的时候,我们需要给它传入一些参数,如下代码所示:
// New创建一个新的Logger。out参数设置日志数据将被写入的目的地
// 参数prefix会在生成的每行日志的最开始出现
// 参数flag定义日志记录包含哪些属性
func New(out io.Writer, prefix string, flag int) *Logger {
return &Logger{out: out, prefix: prefix, flag: flag}
}
上述代码来自 log
包的源代码里的 New
函数的声明。第一个参数 out
指定了日志要写到的目的地。这个参数传入的值必须实现了 io.Writer
接口。第二个参数 prefix
是之前看到的前缀,而日志的标志则是最后一个参数。
以上是我们通过 go 语言自带的 log 来实现的自己的日志工具。Go
社区很强大,社区的大佬们为我们实现了更加强大好用的工具类,比如支持按照日期、大小滚动切割文件输出;有着更细致的日志级别有更高的更好的性能;支持各种插件可以直接对接
elk、prometheus 等。下面为大家介绍两款日志框架:
logrus : https://github.com/sirupsen/logrus
seelog:https://github.com/cihub/seelog
将上面代码中的 main
函数注释掉,修改文件名为 mylog
,包名修改为 package mylog
├── task.go
└── mylog
└── mylog.go
task.go
package main
import "mylog"
func main() {
mylog.Trace.Println("trace")
mylog.Info.Println("info")
mylog.Error.Println("error")
}
可以看到输出为:
INFO: 2020/04/20 18:24:25 task.go:7: info
ERROR: 2020/04/20 18:24:25 task.go:8: error