资讯专栏INFORMATION COLUMN

Golang 源码剖析:log 标准库

stackvoid / 2073人阅读

摘要:源码剖析标准库原文地址源码剖析标准库日志输出构成日期空格时分秒空格内容源码剖析互斥锁,用于确保原子的写入每行需写入的日志前缀内容设置日志辅助信息时间文件名行号的写入。

Golang 源码剖析:log 标准库

原文地址:Golang 源码剖析:log 标准库

日志 输出
2018/09/28 20:03:08 EDDYCJY Blog...
构成

[日期]<空格>[时分秒]<空格>[内容]

源码剖析 Logger
type Logger struct {
    mu     sync.Mutex 
    prefix string
    flag   int
    out    io.Writer
    buf    []byte
}

(1) mu:互斥锁,用于确保原子的写入
(2) prefix:每行需写入的日志前缀内容
(3) flag:设置日志辅助信息(时间、文件名、行号)的写入。可选如下标识位:

const (
    Ldate         = 1 << iota       // value: 1
    Ltime                           // value: 2
    Lmicroseconds                   // value: 4
    Llongfile                       // value: 8
    Lshortfile                      // value: 16
    LUTC                            // value: 32
    LstdFlags     = Ldate | Ltime   // value: 3
)

Ldate:当地时区的格式化日期:2009/01/23

Ltime:当地时区的格式化时间:01:23:23

Lmicroseconds:在 Ltime 的基础上,增加微秒的时间数值显示

Llongfile:完整的文件名和行号:/a/b/c/d.go:23

Lshortfile:当前文件名和行号:d.go:23,会覆盖 Llongfile 标识

LUTC:如果设置 Ldate 或 Ltime,且设置 LUTC,则优先使用 UTC 时区而不是本地时区

LstdFlags:Logger 的默认初始值(Ldate 和 Ltime)

(4) out:io.Writer
(5) buf:用于存储将要写入的日志内容

New
func New(out io.Writer, prefix string, flag int) *Logger {
    return &Logger{out: out, prefix: prefix, flag: flag}
}

var std = New(os.Stderr, "", LstdFlags)

New 方法用于初始化 Logger,接受三个初始参数,可以定制化而在 log 包内默认会初始一个 std,它指向标准输入流。而默认的标准输出、标准错误就是显示器(输出到屏幕上),标准输入就是键盘。辅助的时间信息默认为 Ldate | Ltime,也就是 2009/01/23 01:23:23

// os
var (
    Stdin  = NewFile(uintptr(syscall.Stdin), "/dev/stdin")
    Stdout = NewFile(uintptr(syscall.Stdout), "/dev/stdout")
    Stderr = NewFile(uintptr(syscall.Stderr), "/dev/stderr")
)

Stdin:标准输入

Stdout:标准输出

Stderr:标准错误

Getter

Flags

Prefix

Setter

SetFlags

SetPrefix

SetOutput

Print.., Fatal.., Panic..
func Print(v ...interface{}) {
    std.Output(2, fmt.Sprint(v...))
}

func Printf(format string, v ...interface{}) {
    std.Output(2, fmt.Sprintf(format, v...))
}

func Println(v ...interface{}) {
    std.Output(2, fmt.Sprintln(v...))
}

func Fatal(v ...interface{}) {
    std.Output(2, fmt.Sprint(v...))
    os.Exit(1)
}

func Panic(v ...interface{}) {
    s := fmt.Sprint(v...)
    std.Output(2, s)
    panic(s)
}

...

这一部分介绍最常用的日志写入方法,从源码可得知 XrintlnXrintf 函数 换行可变参数都是通过 fmt 标准库的方法去实现的

FatalPanic 是通过 os.Exit(1)panic(s) 集成实现的。而具体的组装逻辑是通过 Output 方法实现的

Logger.Output
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] != "
" {
        l.buf = append(l.buf, "
")
    }
    _, err := l.out.Write(l.buf)
    return err
}

Output 方法,简单来讲就是将写入的日志事件信息组装并输出,它会根据 flag 标识位的不同来使用 runtime.Caller 去获取当前 goroutine 所执行的函数文件、行号等调用信息(log 标准库中默认深度为 2)。另外如果结尾不是换行符 ,将自动补全一个换行

Logger.formatHeader
func (l *Logger) formatHeader(buf *[]byte, t time.Time, file string, line int) {
    *buf = append(*buf, l.prefix...)
    if l.flag&(Ldate|Ltime|Lmicroseconds) != 0 {
        if l.flag&LUTC != 0 {
            t = t.UTC()
        }
        if l.flag&Ldate != 0 {
            year, month, day := t.Date()
            itoa(buf, year, 4)
            *buf = append(*buf, "/")
            itoa(buf, int(month), 2)
            *buf = append(*buf, "/")
            itoa(buf, day, 2)
            *buf = append(*buf, " ")
        }
        if l.flag&(Ltime|Lmicroseconds) != 0 {
            hour, min, sec := t.Clock()
            itoa(buf, hour, 2)
            *buf = append(*buf, ":")
            itoa(buf, min, 2)
            *buf = append(*buf, ":")
            itoa(buf, sec, 2)
            if l.flag&Lmicroseconds != 0 {
                *buf = append(*buf, ".")
                itoa(buf, t.Nanosecond()/1e3, 6)
            }
            *buf = append(*buf, " ")
        }
    }
    if l.flag&(Lshortfile|Llongfile) != 0 {
        if l.flag&Lshortfile != 0 {
            short := file
            for i := len(file) - 1; i > 0; i-- {
                if file[i] == "/" {
                    short = file[i+1:]
                    break
                }
            }
            file = short
        }
        *buf = append(*buf, file...)
        *buf = append(*buf, ":")
        itoa(buf, line, -1)
        *buf = append(*buf, ": "...)
    }
}

该方法主要是用于格式化日志头(前缀),根据入参不同的标识位,添加分隔符和对应的值到日志信息中。执行流程如下:

(1)如果不是空值,则将 prefix 写入 buf

(2)如果设置 LdateLtimeLmicroseconds,则对应将日期和时间写入 buf

(3)如果设置 LshortfileLlongfile,则对应将文件和行号信息写入 buf

Logger.itoa
func itoa(buf *[]byte, i int, wid int) {
    // Assemble decimal in reverse order.
    var b [20]byte
    bp := len(b) - 1
    for i >= 10 || wid > 1 {
        wid--
        q := i / 10
        b[bp] = byte("0" + i - q*10)
        bp--
        i = q
    }
    // i < 10
    b[bp] = byte("0" + i)
    *buf = append(*buf, b[bp:]...)
}

该方法主要用于将整数转换为定长的十进制 ASCII,同时给出负数宽度避免左侧补 0。另外会以相反的顺序组合十进制

如何定制化 Logger

在标准库内,可通过其开放的 New 方法来实现各种各样的自定义 Logger 组件,但是为什么也可以直接 log.Print* 等方法呢?

func New(out io.Writer, prefix string, flag int) *Logger

其实是在标准库内,如果你刚刚细心的看了前面的小节,不难发现其默认实现了一个 Logger 组件

var std = New(os.Stderr, "", LstdFlags)

这也是一个小小的精妙之处 ⭕️

总结

通过查阅 log 标准库的源码,可得知最简单的一个日志包应该如何编写。另外 log 包是在所有涉及到 Logger 的地方都对 sync.Mutex 进行操作(以此解决原子问题),其余逻辑均为组装日志信息和转换数值格式,该包较为经典,可以多读几遍

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/29470.html

相关文章

  • Golang 源码剖析:fmt 标准 --- Print* 是怎么样输出的?

    摘要:源码剖析标准库是怎么样输出的原文地址源码剖析标准库前言标准开场见多了,那内部标准库又是怎么输出这段英文的呢今天一起来围观下源码吧 Golang 源码剖析:fmt 标准库 --- Print* 是怎么样输出的? 原文地址:Golang 源码剖析:fmt 标准库 前言 package main import ( fmt ) func main() { fmt.Print...

    charles_paul 评论0 收藏0
  • 重拾golang - go目录结构说明

    摘要:目录结构说明集多编程范式之大成者,使开发者能够快速的开发测试部署程序,支持全平台静态编译。上目录位置主要目录包含如下图,分别进行说明文件夹存放检查器的辅助文件。工作区有个子目录目录目录和目录。目录用于以代码包的形式组织并保存源码文件。 go 目录结构说明   golang集多编程范式之大成者,使开发者能够快速的开发、测试、部署程序,支持全平台静态编译。go具有优秀的依赖管理,高效的运行...

    zhisheng 评论0 收藏0
  • Derek解读Bytom源码-Api Server接口服务

    摘要:首先读取请求内容,解析请求,接着匹配相应的路由项,随后调用路由项的回调函数来处理。每一个路由项由请求方法和回调函数组成将监听地址作为参数,最终执行开始服务于外部请求创建对象首先,实例化对象。我们可以看到一条项由和对应的回调函数组成。 作者:Derek 简介 Github地址:https://github.com/Bytom/bytom Gitee地址:https://gitee.com...

    GitCafe 评论0 收藏0
  • PHPer书单

    摘要:想提升自己,还得多看书多看书多看书下面是我收集到的一些程序员应该看得书单及在线教程,自己也没有全部看完。共勉吧当然,如果你有好的书想分享给大家的或者觉得书单不合理,可以去通过进行提交。讲师温铭,软件基金会主席,最佳实践作者。 想提升自己,还得多看书!多看书!多看书!下面是我收集到的一些PHP程序员应该看得书单及在线教程,自己也没有全部看完。共勉吧!当然,如果你有好的书想分享给大家的或者...

    jimhs 评论0 收藏0
  • Golang 大杀器之性能剖析 PProf

    摘要:大杀器之性能剖析原文地址大杀器之性能剖析前言写了几吨代码,实现了几百个接口。功能测试也通过了,终于成功的部署上线了结果,性能不佳,什么鬼 Golang 大杀器之性能剖析 PProf 原文地址:Golang 大杀器之性能剖析 PProf 前言 写了几吨代码,实现了几百个接口。功能测试也通过了,终于成功的部署上线了 结果,性能不佳,什么鬼?

    leeon 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<