package main
import (
"log"
)
// 使用init 函数,这个函数在main 函数执行之前就可以初始化
// 自定义的日志抬头信息为:时间 + 文件名 + 源代码所在行号(就是log.Ldata | log.Lshortfile)
// 通过log.SetFlags 进行设置
func init() {
//log.SetFlags(log.Ldate | log.Lshortfile) //日期+文件和行号
//log.SetFlags(log.Ldate | log.Ltime | log.LUTC) //日期 + 时间 + 日期转为 0 时区的
//区分业务:
log.SetPrefix("[UserCenter]")
log.SetFlags(log.LstdFlags | log.Lshortfile | log.LUTC)
}
func main() {
// 用法和fmt 包很相似,但是它的默认输出带了时间戳
//没有写 init()函数时的输出
log.Println("今天去拉萨!") // 2020/08/05 10:08:18 今天去拉萨!
log.Println("明天去北京!") // 2020/08/05 10:08:18 明天去北京!
//写了init()函数后上面俩句的输出:
// 2020/08/05 main.go:11: 今天去拉萨!
// 2020/08/05 main.go:12: 明天去北京!
//log 包提供的可以定义的选项常量
/*注意:如果设置了 Lmicroseconds,那么Ltime 就不生效了
如果设置了 Lshortfile,Llongfile 也不生效了
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提供的标准抬头信息
)
*/
//LUTC比较特殊:如果配置了时间标签,如果设置了TUTC,就会把输出的日期时间转为0 时区的日期时间显示
//修改 init()函数,添加TUTC ,对于东八区来说,就会减去8个小时
//此时 上面的2 个输出是:
// 2020/08/05 02:44:28 今天去拉萨!
// 2020/08/05 02:44:28 明天去北京!
// LstdFlags 表示标准的日志抬头信息,也就是默认的,包含日期和具体时间
//大部分情况下,都有很多业务,每个业务都需要记录日志,那怎么区分业务呢?
//就是设置日志的前缀,通过 log.SetPrefix 可以指定输出日志的前缀,指定为【UserCenter】
//比如一个用户中心系统的日志,就可以看到这些日志是属于哪些业务了
//上边的 2个输出:
// [UserCenter]2020/08/05 02:50:55 main.go:22: 今天去拉萨!
// [UserCenter]2020/08/05 02:50:55 main.go:23: 明天去北京!
}
log 还有Fatal 和 Panic 系列的函数
Fatal 表示程序遇到了致命的错误,需要退出,这时使用 Fatal 记录日志后,然后程序退出
也就是说 Fatal 相当于先调用Print 打印日志,然后再调用 os.Exit(1) 退出程序
Panic 系列的函数,表示先使用Print 记录日志,然后调用 panic() 函数抛出一个恐慌
这时除非使用recover() 函数,否则程序就会打印错误堆栈信息,然后程序终止
实现原理
日志包 log 关键的输出日志就在于 std.Output 方法
func New(out io.Writer, prefix string, flag int) *Logger {
return &Logger{out: out, prefix: prefix, flag: flag}
}
var std = New(os.Stderr, "", LstdFlags)
// 从源码可以看出,变量 std 其实是一个 *Logger,通过 log.New函数创建,默认输出到 os.Stderr 设备,前缀为空,日志抬头信息为标准抬头 LstdFlags。
// os.Stderr对应的是UNIX里的标准错误警告信息的输出设备,同时被作为默认的日志输出目的地。
// 除此之外,还有标准输出设备os.Stdout以及标准输入设备os.Stdin。
var (
Stdin = NewFile(uintptr(syscall.Stdin), "/dev/stdin")
Stdout = NewFile(uintptr(syscall.Stdout), "/dev/stdout")
Stderr = NewFile(uintptr(syscall.Stderr), "/dev/stderr")
)
// 以上就是定义的UNIX的标准的三种设备,分别用于输入、输出和警告错误信息。理解了os.Stderr,
// 现在我们看下Logger这个结构体,日志的信息和操作,都是通过这个Logger操作的。
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里。
//了解了结构体Logger的字段,现在就可以看下它最重要的方法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 {
//因为runtime.Caller代价比较大,先不加锁
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')
}
//输出拼接好的缓冲buf里的日志信息到目的地
_, err := l.out.Write(l.buf)
return err
}
//整个代码比较简洁,为了多goroutine安全互斥锁也用上了,但是在获取调用堆栈信息的时候,又要先解锁,因为这个过程比较重。
//获取到文件、行号等信息后,继续加互斥锁保证安全。
//后面的就比较简单了,formatHeader方法主要是格式化日志抬头信息,然后存储在buf这个缓冲中,
//最后再把我们自己的日志信息拼接到缓冲buf的后面,然后为一次log日志输出追加一个换行符,这样每次日志输出都是一行一行的。
//有了最终的日志信息buf,然后把它写到输出的目的地out里就可以了,这是一个实现了io.Writer接口的类型,只要实现了这个接口,都可以当作输出目的地。
func (l *Logger) SetOutput(w io.Writer) {
l.mu.Lock()
defer l.mu.Unlock()
l.out = w
}
//log包的SetOutput函数,可以设置输出目的地。这里稍微简单介绍下runtime.Caller,它可以获取运行时方法的调用信息。
func Caller(skip int) (pc uintptr, file string, line int, ok bool)
//参数skip表示跳过栈帧数,0表示不跳过,也就是runtime.Caller的调用者。1的话就是再向上一层,表示调用者的调用者。
//log日志包里使用的是2,也就是表示我们在源代码中调用log.Print、log.Fatal和log.Panic这些函数的调用者。
//以main函数调用log.Println为例,是main->log.Println->*Logger.Output->runtime.Caller这么一个方法调用栈,所以这时候,skip的值分别代表:
//0表示*Logger.Output中调用runtime.Caller的源代码文件和行号
//1表示log.Println中调用*Logger.Output的源代码文件和行号
//2表示main中调用log.Println的源代码文件和行号
//所以这也是log包里的这个skip的值为什么一直是2的原因。