如何在Go的函數中得到調用者函數名?
有時候在Go的函數調用的過程中,我們需要知道函數被誰調用,比如打印日志信息等。例如下面的函數,我們希望在日志中打印出調用者的名字。
- func Foo() {
- fmt.Println("誰在調用我?")
- bar()
- }
- func Bar() {
- fmt.Println("誰又在調用我?")
- }
首先打印函數本身的名稱
最簡單的方式就是硬編碼。 因為在編譯之前,我們肯定知道打印的時候所在哪個函數,但是更好的方式是編寫一個通用的函數,比如下面的例子:
- package main
- import (
- "fmt"
- "runtime"
- )
- func main() {
- Foo()
- }
- func Foo() {
- fmt.Printf("我是 %s, 誰在調用我?\n", printMyName())
- Bar()
- }
- func Bar() {
- fmt.Printf("我是 %s, 誰又在調用我?\n", printMyName())
- }
- func printMyName() string {
- pc, _, _, _ := runtime.Caller(1)
- return runtime.FuncForPC(pc).Name()
- }
輸出結果:
- 我是 main.Foo, 誰在調用我?
- 我是 main.Bar, 誰又在調用我?
可以看到函數在被調用的時候,printMyName把函數本身的名字打印出來了,注意這里Caller的參數是1, 因為我們將業務代碼封裝成了一個函數。
首先打印函數調用者的名稱
將上面的代碼修改一下,增加一個新的printCallerName的函數,可以打印調用者的名稱。
- func main() {
- Foo()
- }
- func Foo() {
- fmt.Printf("我是 %s, %s 在調用我!\n", printMyName(), printCallerName())
- Bar()
- }
- func Bar() {
- fmt.Printf("我是 %s, %s 又在調用我!\n", printMyName(), printCallerName())
- }
- func printMyName() string {
- pc, _, _, _ := runtime.Caller(1)
- return runtime.FuncForPC(pc).Name()
- }
- func printCallerName() string {
- pc, _, _, _ := runtime.Caller(2)
- return runtime.FuncForPC(pc).Name()
- }
相關函數介紹
你可以通過runtime.Caller、runtime.Callers、runtime.FuncForPC等函數更詳細的跟蹤函數的調用堆棧。
1、func Caller(skip int) (pc uintptr, file string, line int, ok bool)
Caller可以返回函數調用棧的某一層的程序計數器、文件信息、行號。
0 代表當前函數,也是調用runtime.Caller的函數。1 代表上一層調用者,以此類推。
2、func Callers(skip int, pc []uintptr) int
Callers用來返回調用站的程序計數器, 放到一個uintptr中。
0 代表 Callers 本身,這和上面的Caller的參數的意義不一樣,歷史原因造成的。 1 才對應這上面的 0。
比如在上面的例子中增加一個trace函數,被函數Bar調用。
- ……
- func Bar() {
- fmt.Printf("我是 %s, %s 又在調用我!\n", printMyName(), printCallerName())
- trace()
- }
- func trace() {
- pc := make([]uintptr, 10) // at least 1 entry needed
- n := runtime.Callers(0, 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())
- }
- }
輸出結果可以看到這個goroutine的整個棧都打印出來了:
- /usr/local/go/src/runtime/extern.go:218 runtime.Callers
- /Users/yuepan/go/src/git.intra.weibo.com/platform/tool/g/main.go:34 main.trace
- /Users/yuepan/go/src/git.intra.weibo.com/platform/tool/g/main.go:20 main.Bar
- /Users/yuepan/go/src/git.intra.weibo.com/platform/tool/g/main.go:15 main.Foo
- /Users/yuepan/go/src/git.intra.weibo.com/platform/tool/g/main.go:10 main.main
- /usr/local/go/src/runtime/proc.go:210 runtime.main
- /usr/local/go/src/runtime/asm_amd64.s:1334 runtime.goexit
3、func CallersFrames(callers []uintptr) *Frames
上面的Callers只是或者棧的程序計數器,如果想獲得整個棧的信息,可以使用CallersFrames函數,省去遍歷調用FuncForPC。
上面的trace函數可以更改為下面的方式:
- func trace2() {
- pc := make([]uintptr, 10) // at least 1 entry needed
- n := runtime.Callers(0, pc)
- frames := runtime.CallersFrames(pc[:n])
- for {
- frame, more := frames.Next()
- fmt.Printf("%s:%d %s\n", frame.File, frame.Line, frame.Function)
- if !more {
- break
- }
- }
- }
4、func FuncForPC(pc uintptr) *Func
FuncForPC 是一個有趣的函數, 它可以把程序計數器地址對應的函數的信息獲取出來。如果因為內聯程序計數器對應多個函數,它返回最外面的函數。
它的返回值是一個*Func類型的值,通過*Func可以獲得函數地址、文件行、函數名等信息。
除了上面獲取程序計數器的方式,也可以通過反射的方式獲取函數的地址:
- runtime.FuncForPC(reflect.ValueOf(foo).Pointer()).Name()
5、獲取程序堆棧
在程序panic的時候,一般會自動把堆棧打出來,如果你想在程序中獲取堆棧信息,可以通過debug.PrintStack()打印出來。比如你在程序中遇到一個Error,但是不期望程序panic,只是想把堆棧信息打印出來以便跟蹤調試,你可以使用debug.PrintStack()。
抑或,你自己讀取堆棧信息,自己處理和打印:
- func DumpStacks() {
- buf := make([]byte, 16384)
- buf = buf[:runtime.Stack(buf, true)]
- fmt.Printf("=== BEGIN goroutine stack dump ===\n%s\n=== END goroutine stack dump ===", buf)
- }
參考 調試利器:dump goroutine 的 stacktrace。
利用堆棧信息還可以獲取goroutine的id, 參考: 再談談獲取 goroutine id 的方法
- func GoID() int {
- var buf [64]byte
- n := runtime.Stack(buf[:], false)
- idField := strings.Fields(strings.TrimPrefix(string(buf[:n]), "goroutine "))[0]
- id, err := strconv.Atoi(idField)
- if err != nil {
- panic(fmt.Sprintf("cannot get goroutine id: %v", err))
- }
- return id
- }