背景当我们在应用代码中添加业务日志时,无论是什么级别的日志,除了我们的active信息之外传给Logger让它记录,日志是由哪个函数打印的,位置在哪里,也是很重要的信息,否则排错的时候可能会大海捞针。记录日志时,记录调用Logger方法的调用者的函数名和行号。一些日志库支持,比如Zap:funcmain(){logger,_:=zap.NewProduction(zap.AddCaller())deferlogger.Sync()logger.Info("helloworld")}output:{"level":"info","ts":1587740198.9508286,"caller":"caller/main.go:9","msg":"helloworld"}然而,如果你想构建一个健壮的开发框架,你不应该让自己一个日志库强绑定。更好的方法是开发一个日志门面,在程序中直接使用日志门面,然后调用日志库完成日志记录。典型的Javaslf4j就是这样的思路。程序直接使用了slf4j,后续的Logger可以是logback也可以是log4j甚至是任何符合slf4j协议的日志库实现。如果我们用Go设计一个LogFacade,我们需要在facade中获取调用者的函数名和文件位置,那么这个功能在Go中如何实现呢?这就需要运行时标准库提供的Caller函数。本文主要介绍runtime.Caller的使用。上面说了这么多只是为了铺路,学习它,在哪里可以应用。runtime.Callerruntime.Caller的函数签名如下:funcCaller(skipint)(pcuintptr,filestring,lineint,okbool)Caller函数会上报调用执行的函数的文件和行号信息当前Go程序的堆栈。参数skip是要回溯的栈帧数,0表示Caller的调用者(Caller所在的调用栈),1表示调用Caller的调用者的调用者,以此类推。是不是有点晕,这里举个例子:funcCallerA(){//获取CallerA函数的调用栈pc,file,lineNo,ok:=runtime.Caller(0)//获取CallerA函数的调用调用者的调用堆栈pc1,file1,lineNo1,ok1:=runtime.Caller(1)}函数的返回值是调用堆栈标识符、带路径的完整文件名以及调用在文件中的行号。如果无法获取信息,则返回值ok将设置为false。获取调用者的函数名runtime.Caller返回值中第一个返回值是一个调用栈标识,通过它我们可以获取调用栈的函数信息*runtime.Func,进而进一步获取调用者的函数名,这里的将使用的功能和方法如下。funcFuncForPC(pcuintptr)*Funcfunc(*Func)Nameruntime.FuncForPC函数返回一个*Func表示调用栈标识pc对应的调用栈;如果调用栈标识符没有对应的调用栈,函数将返回nil。Name方法返回调用堆栈调用的函数的名称。上面说了runtime.FuncForPC可能会返回nil,但是Name方法在实现的时候对这种情况做了判断,避免了panic的可能,所以我们可以放心大胆的使用。func(f*Func)Name()string{iff==nil{return""}fn:=f.raw()iffn.isInlined(){//内联版本fi:=(*funcinl)(unsafe.Pointer(fn))returnfi.name}returnfuncname(f.funcInfo())}使用示例下面看一个简单的例子,一起使用runtime.Caller和runtime.FuncForPC获取调用者信息:packagemainimport("fmt""path""runtime")funcgetCallerInfo(skipint)(infostring){pc,file,lineNo,ok:=runtime.Caller(skip)if!ok{info="runtime.Caller()failed"return}funcName:=runtime.FuncForPC(pc).Name()fileName:=path.Base(file)//基函数返回路径的最后一个元素returnfmt.Sprintf("FuncName:%s,file:%s,line:%d",funcName,fileName,lineNo)}funcmain(){//打印出getCallerInfo函数本身的信息fmt.Println(getCallerInfo(0))//打印出getCallerInfo函数调用者的信息fmt.Println(getCallerInfo(1))}注:这里演示的是比较简单的方式,信息可以通过跟踪调用堆栈来获得调用者的信息。当你真正要实现一个类库比如日志门面的时候,可能会有好几层封装。日志中要记录的调用者信息应该是业务代码中日志所在的位置。这时候要回溯的层数肯定不是1那么简单,跳多少层取决于具体实现的日志门面的封装。小结今天介绍了通过runtime.Caller回溯调用栈获取调用者信息的方法。虽然它很强大,但是频繁获取这些信息也会对程序性能产生影响。我们的业务代码不应该依赖它来实现,它更多的作用是在记录信息的时候会用到一些对业务透明的类库。
