在日志信息中,有时需要获取当前函数的调用信息,比如打印调用栈:

调用栈

debug.Stack() []byte

debug.Stack()函数可以获取当前 goroutine 的调用栈信息,需要import "runtime/debug"

 8
 9
10
11
12
13
14
15
16
17
18
19
20
func foo() {
    fmt.Println("--- BEGIN ---")
    fmt.Println(string(debug.Stack()))
    fmt.Println("--- END ---")
}

func boom() {
    foo()
}

func main() {
    boom()
}

可以获得类似输出结果:

--- BEGIN ---
goroutine 1 [running]:
runtime/debug.Stack(0xc00006ef58, 0x1, 0x1)
    /usr/local/Cellar/go/1.11.2/libexec/src/runtime/debug/stack.go:24 +0xa7
main.foo()
    xxx/trace.go:10 +0x66
main.boom()
    xxx/trace.go:15 +0x20
main.main()
    xxx/trace.go:19 +0x20

--- END ---

runtime.Stack(buf []byte, all bool) int

debug.Stack()函数源码在runtime/debug/stack.go中:

func Stack() []byte {
    buf := make([]byte, 1024)
    for {
        n := runtime.Stack(buf, false)
        if n < len(buf) {
            return buf[:n]
        }
        buf = make([]byte, 2*len(buf))
    }
}

可以看到它是通过调用runtime.Stack获取的调用栈信息, 其中第二个参数all表示是否获取所有 goroutine 的调用栈。

可以将第二个参数改为true然后创建多个 goroutine 查看输出效果。

更可控的调用信息

上面两个函数虽然可以获取调用栈,但是直接将所有调用信息返回,控制参数较少。

下面几个函数可以实现获取单层的调用信息。

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

runtime.Caller函数可以获取调用者的信息

  • 参数skip:表示跳过的层级,相当于往上获取第几层的调用者。0 表示当前函数,1 表示上一层函数,依次往上。

  • 返回值

    • pcprogram counter(不知道该怎么翻译,程序计数器?),可以大概理解为函数编号,可以通过runtime.FuncForPC解析
    • file:文件名
    • line:行号
 8
 9
10
11
12
13
func foo() {
    pc, file, line, ok := runtime.Caller(1)
    if ok {
        fmt.Println(runtime.FuncForPC(pc).Name(), file, line)
    }
}

可以得到这样一条输出:

main.boom xxx/trace.go 16

runtime.Callers(skip int, pc []uintptr) int

runtime.Callers一次可以获取调用栈中多个program counter

但是第一个参数skip与上面的runtime.Caller稍有不同,如果传0会获取到Callers函数本身的信息。 传1时才与上面的Caller函数对应。

 8
 9
10
11
12
13
14
15
16
func foo() {
    pc := make([]uintptr, 10)
    n := runtime.Callers(1, pc)
    for i := 0; i < n; i++ {
        f := runtime.FuncForPC(pc[i])
        file, line := f.FileLine(pc[i])
        fmt.Printf("%s %d %s\n", file, line, f.Name())
    }
}

输出:

xxx/trace.go 10 main.foo
xxx/trace.go 20 main.boom
xxx/trace.go 24 main.main
/usr/local/Cellar/go/1.11.2/libexec/src/runtime/proc.go 210 runtime.main
/usr/local/Cellar/go/1.11.2/libexec/src/runtime/asm_amd64.s 1334 runtime.goexit

runtime.CallersFrames(callers []uintptr) *runtime.Frames

runtime.CallersFrames可以一次解析多个pc

 8
 9
10
11
12
13
14
15
16
17
18
19
func foo() {
    pc := make([]uintptr, 10)
    n := runtime.Callers(1, pc)
    frames := runtime.CallersFrames(pc[:n])

    var frame runtime.Frame
    more := n > 0
    for more {
        frame, more = frames.Next()
        fmt.Printf("%s %d %s\n", frame.File, frame.Line, frame.Function)
    }
}

func runtime.FuncForPC(pc uintptr) *runtime.Func

前面使用了runtime.FuncForPC来解析pc,获得函数信息。

它返回的*runtime.Func主要有三个方法:

  • Entry() uintptr: 返回函数入口地址(函数注释Entry address of the function)
  • Name() string: 返回函数名
  • FileLine(pc uintptr) (file string, line int): 返回文件名和行号