如何在Go函数中获取调用者的函数名、文件名、行号-《GO开发知识笔记》

admin 2025-11-04 00:59:42 编程 来源:ZONE.CI 全球网 0 阅读模式
  • 背景
  • runtime.Caller
  • 获取调用者的函数名
  • 使用示例
  • 总结

    背景

    我们在应用程序的代码中添加业务日志的时候,不论是什么级别的日志,除了我们主动传给 Logger 让它记录的信息外,这行日志是由哪个函数打印的、所在的位置也是非常重要的信息,不然排查问题的时候很有可能就犹如大海捞针。对于在记录日志时记录调用 Logger 方法的调用者的函数名、行号这些信息。有的日志库支持,比如 Zap

    1. func main() {
    2. logger, _ := zap.NewProduction(zap.AddCaller())
    3. defer logger.Sync()
    4. logger.Info("hello world")
    5. }

    输出:

    1. {"level":"info","ts":1587740198.9508286,"caller":"caller/main.go:9","msg":"hello world"}

    不过如果想搞一个健壮的开发框架,不应该让自己跟某个日志库强绑定,更好的方法是开发一个日志的门面,程序里直接使用日志门面,再由门面调用日志库完成日志的记录。典型的 Java 的 slf4j 就是这个思路,程序里直接使用的是slf4j ,后面的 Logger 可以是 logback 也可以是 log4j 甚至是任何满足 slf4j 约定的日志库实现。如果让我们用 Go 设计一个Log Facade,就需要我们自己在门面里获取调用者的函数名、文件位置了,那么在Go里面怎么实现这个功能呢?这就需要借助 runtime 标准库提供的 Caller 函数了。本文主要介绍 runtime.Caller 的使用,上面说了那么多只是为了铺垫一下,学会它,在哪些地方可以应用上。

    runtime.Caller

    runtime.Caller 的函数签名如下:

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

    Caller 函数会报告当前 Go 程序调用栈所执行的函数的文件和行号信息。参数skip为要上溯的栈帧数,0 表示Caller的调用者(Caller所在的调用栈),1 表示调用 Caller 调用者的调用者,以此类推。是不是有点晕,这里举个例子

    1. func CallerA() {
    2. //获取的是 CallerA 这个函数的调用栈
    3. pc, file, lineNo, ok := runtime.Caller(0)
    4. //获取的是 CallerA函数的调用者的调用栈
    5. pc1, file1, lineNo1, ok1 := runtime.Caller(1)
    6. }

    函数的返回值为调用栈标识符、带路径的完整文件名、该调用在文件中的行号。如果无法获得信息,返回值 ok 会被设为 false。

    获取调用者的函数名

    runtime.Caller 返回值中第一个返回值是一个调用栈标识,通过它我们能拿到调用栈的函数信息 *runtime.Func,再进一步获取到调用者的函数名字,这里面会用到的函数和方法如下。

    1. func FuncForPC(pc uintptr) *Func
    2. func (*Func) Name

    runtime.FuncForPC 函数返回一个表示调用栈标识符pc对应的调用栈的*Func;如果该调用栈标识符没有对应的调用栈,函数会返回nil。Name 方法返回该调用栈所调用的函数的名字,上面说了runtime.FuncForPC 有可能会返回 nil,不过Name方法在实现的时候做了这种情况的判断,避免出现panic 的可能,所以我们可以放心大胆的使用。

    1. func (f *Func) Name() string {
    2. if f == nil {
    3. return ""
    4. }
    5. fn := f.raw()
    6. if fn.isInlined() { // inlined version
    7. fi := (*funcinl)(unsafe.Pointer(fn))
    8. return fi.name
    9. }
    10. return funcname(f.funcInfo())
    11. }

    使用示例

    下面看一个使用 runtime.Caller 和 runtime.FuncForPC 一起配合获取调用者信息的简单例子

    1. package main
    2. import (
    3. "fmt"
    4. "path"
    5. "runtime"
    6. )
    7. func getCallerInfo(skip int) (info string) {
    8. pc, file, lineNo, ok := runtime.Caller(skip)
    9. if !ok {
    10. info = "runtime.Caller() failed"
    11. return
    12. }
    13. funcName := runtime.FuncForPC(pc).Name()
    14. fileName := path.Base(file) // Base函数返回路径的最后一个元素
    15. return fmt.Sprintf("FuncName:%s, file:%s, line:%d ", funcName, fileName, lineNo)
    16. }
    17. func main() {
    18. // 打印出getCallerInfo函数自身的信息
    19. fmt.Println(getCallerInfo(0))
    20. // 打印出getCallerInfo函数的调用者的信息
    21. fmt.Println(getCallerInfo(1))
    22. }

    注意:这里我们演示地比较简单,往上追溯一个调用栈就能拿到调用者的信息。真正要实现日志门面之类的类库的时候,可能是会有几层封装,想在日志里记录的调用者信息应该是业务代码中打日志的位置,这时要向上回溯的层数肯定就不是 1 这么简单了,具体跳过几层要看实现的日志门面具体的封装情况。

    总结

    今天介绍了通过 runtime.Caller 回溯调用栈获取调用者的信息的方法,虽然强大,不过频繁获取这个信息也是会对程序性能有影响。我们的业务代码不应该依赖于它来实现,它发挥作用的地方更多的是对业务透明的一些类库在记录信息的时候才会被用到。

    以太坊cppgolang区别 编程

    以太坊cppgolang区别

    以太坊是一种去中心化的开源平台,它采用智能合约技术,旨在构建和运行不受干扰的分布式应用程序。作为目前最受欢迎的区块链平台之一,以太坊提供了多种编程语言的支持,其
    progolang 编程

    progolang

    Go语言(Golang)是由Google开发的一门静态类型编程语言。作为一名专业的Golang开发者,我深知这门语言的优势和特点。在本文中,我将介绍Golang
    golangn个发送者 编程

    golangn个发送者

    Golang是一种开源的编程语言,由Google团队开发,旨在提高程序的并发性和简化软件开发过程。在Go语言中,有时需要向多个接收者发送信息。本文将介绍如何在G
    golang技能图谱 编程

    golang技能图谱

    从互联网行业的快速发展到人工智能技术的日益成熟,各种编程语言也应运而生。而在这众多的编程语言中,Golang(即Go)作为一门强大且高效的开发语言备受关注。Go
    评论:0   参与:  9