本文首发于我的个人博客

这篇文章介绍了作者在参与一个golang日志系统的开发的时候,解决需要打印出执行日志打印操作时的业务函数名,业务文件名与所在行数的需求过程中,遇到的问题和解决方案

需求场景

在平日里使用日志的时候,一个好的日志系统,往往会打印出类似如下的信息

<log_level>:<log_message>:<package_path>/<filename>:<line_no>:<function_name>
比如
INFO:connect to sql:/users/admin/home/go/src/io/rivers/demoProject/main.go:45:io.rivers.demoProject.testFunction

这样子在打印出日志等级,日志消息的同时,输出业务逻辑所在的文件,行数,函数,对后期的bug排查,性能分析都有很大的帮助

那么,如何在golang中实现这一功能呢?

实现方式

runtime.Callerruntime.FuncForPC

先看一下二者的函数签名

func Caller(skip int) (pc uintptr, file string, line int, ok bool)

func FuncForPC(pc uintptr) *Func

单看函数签名就比较容易了解到:

runtime.Callerruntime.FuncForPC
runtime.FuncForPCruntime.Caller
skip
skip == 0skip == 1skip == 2

一般情况下这两个函数都是连在一起使用,如

// 获取上层调用者PC,文件名,所在行
pc, codePath, codeLine, ok := runtime.Caller(1)
if !ok{
    // 不ok,函数栈用尽了
    code = "-"
    func = "-"
} else {
    // 拼接文件名与所在行
    code = fmt.Sprintf("%s:%d", codePath, codeLine)
    // 根据PC获取函数名
    func = runtime.FuncForPC(pc).Name()
}

实现重点与自动获取的优化

runtime.Callerruntime.FuncForPCruntime.Callerskip
skip
  1. 写死
  2. 尝试自动获取

听起来第二种方法要比第一种方法好,但是事实上并不是这样的,在看完实现之后,大家就会明白了

skip
logloglog
logruntime.Callerskip1
func log(logLevel int, logMessage string) {
    //....
    pc, file, line, ok := runtime.Caller(1)
    //....
}
skip
func log(logLevel int, logMessage string) {
    //....
    logHelper(logLevel, logMessage)
    //....
}

func logHelper(logLevel int, logMessage string) {
    //....
    logReal(logLevel, logMessage)
    //....
}

func logReal(logLevel int, logMessage string) {
    //...
    pc, file, line, ok := runtime.Caller(3)
    //...
}
skip

尝试自动获取

这次的尝试自动获取是我在编写日志系统时遇到的一个比较特殊的情况

在上面说的#将skip写死中,其实我们有一个重要的前提,那就是

log

但是这次在开发日志系统时,遇到了这样的场景:

log1log2log2log1log2log1log1runtime.Callerruntime.FuncForPC
skip
FuncForPCskip

实现:

for skip := 1; true; skip++ {
    pc, codePath, codeLine, ok := runtime.Caller(skip)
    if !ok{
        // 不ok,函数栈用尽了
        auto.Code = prevCode
        auto.Func = prevFunc
        return auto
    } else{
        prevCode = fmt.Sprintf("%s:%d", codePath, codeLine)
        prevFunc = runtime.FuncForPC(pc).Name()
        auto.Code = prevCode
        auto.Func = prevFunc
        if !strings.Contains(prevFunc, "<package_name>") {
            // 找到包外的函数了
            return auto
        }
    }
}

这样就算是一个能够解决问题的方案了