如何在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
等函式更詳細的跟蹤函式的呼叫堆疊。
func Caller(skip int) (pc uintptr, file string, line int, ok bool)
Caller
可以返回函式呼叫棧的某一層的程式計數器、檔案資訊、行號。
0 代表當前函式,也是呼叫runtime.Caller
的函式。1 代表上一層呼叫者,以此類推。
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
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 } } }
func FuncForPC(pc uintptr) *Func
FuncForPC
是一個有趣的函式, 它可以把程式計數器地址對應的函式的資訊獲取出來。如果因為內聯程式計數器對應多個函式,它返回最外面的函式。
它的返回值是一個*Func
型別的值,通過*Func
可以獲得函式地址、檔案行、函式名等資訊。
除了上面獲取程式計數器的方式,也可以通過反射的方式獲取函式的地址:
runtime.FuncForPC(reflect.ValueOf(foo).Pointer()).Name()
獲取程式堆疊
在程式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) }
參考ofollow,noindex">除錯利器: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 }