Go語言的函數調用資訊

來源:互聯網
上載者:User
這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。

By chaishushan{AT}gmail.com

注: 本文初稿發在 Golang 中國部落格, 這裡的內容有部分修改.

函數的調用資訊是程式中比較重要運行期資訊, 在很多場合都會用到(比如調試或日誌).

Go語言 runtime 包的 runtime.Caller / runtime.Callers / runtime.FuncForPC 等幾個函數提供了擷取函數調用者資訊的方法.

這幾個函數的文檔連結:

  • http://docs.studygolang.com/pkg/runtime/#Caller
  • http://docs.studygolang.com/pkg/runtime/#Callers
  • http://docs.studygolang.com/pkg/runtime/#FuncForPC

本文主要講述這幾個函數的用法.

runtime.Caller 的用法

函數的簽名如下:

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

runtime.Caller 返回當前 goroutine 的棧上的函數調用資訊. 主要有當前的 pc 值和調用的檔案和行號等資訊. 若無法獲得資訊, 返回的 ok 值為 false.

其輸入參數 skip 為要跳過的棧幀數, 若為 0 則表示 runtime.Caller 的調用者.

注意:由於曆史原因, runtime.Callerruntime.Callers 中的 skip 含義並不相同, 後面會講到.

下面是一個簡單的例子, 列印函數調用的棧幀資訊:

func main() {    for skip := 0; ; skip++ {        pc, file, line, ok := runtime.Caller(skip)        if !ok {            break        }        fmt.Printf("skip = %v, pc = %v, file = %v, line = %v\n", skip, pc, file, line)    }    // Output:    // skip = 0, pc = 4198453, file = caller.go, line = 10    // skip = 1, pc = 4280066, file = $(GOROOT)/src/pkg/runtime/proc.c, line = 220    // skip = 2, pc = 4289712, file = $(GOROOT)/src/pkg/runtime/proc.c, line = 1394}

其中 skip = 0 為當前檔案(“caller.go”)的 main.main 函數, 以及對應的行號. 這裡省略的無關代碼, 因此輸出的行號和網頁展示的位置有些差異.

另外的 skip = 1skip = 2 也分別對應2個函數調用. 通過查閱 runtime/proc.c 檔案的代碼, 我們可以知道對應的函數分別為 runtime.mainruntime.goexit.

整理之後可以知道, Go的普通程式的啟動順序如下:

  1. runtime.goexit 為真正的函數入口(並不是main.main)
  2. 然後 runtime.goexit 調用 runtime.main 函數
  3. 最終 runtime.main 調用使用者編寫的 main.main 函數

runtime.Callers 的用法

函數的簽名如下:

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

runtime.Callers 函數和 runtime.Caller 函數雖然名字相似(多一個尾碼s), 但是函數的參數/傳回值和參數的意義都有很大的差異.

runtime.Callers 把調用它的函數Go程棧上的程式計數器填入切片 pc 中. 參數 skip 為開始在 pc 中記錄之前所要跳過的棧幀數, 若為0則表示 runtime.Callers 自身的棧幀, 若為1則表示調用者的棧幀. 該函數返回寫入到 pc 切片中的項數(受切片的容量限制).

下面是 runtime.Callers 的例子, 用於輸出每個棧幀的 pc 資訊:

func main() {    pc := make([]uintptr, 1024)    for skip := 0; ; skip++ {        n := runtime.Callers(skip, pc)        if n <= 0 {            break        }        fmt.Printf("skip = %v, pc = %v\n", skip, pc[:n])    }    // Output:    // skip = 0, pc = [4304486 4198562 4280114 4289760]    // skip = 1, pc = [4198562 4280114 4289760]    // skip = 2, pc = [4280114 4289760]    // skip = 3, pc = [4289760]}

輸出新的 pc 長度和 skip 大小有逆相關性. skip = 0runtime.Callers 自身的資訊.

這個例子比前一個例子多輸出了一個棧幀, 就是因為多了一個runtime.Callers棧幀的資訊(前一個例子是沒有runtime.Caller資訊的(注意:沒有s尾碼)).

那麼 runtime.Callersruntime.Caller 有哪些關聯和差異?

runtime.Callersruntime.Caller 的異同

因為前面2個例子為不同的程式, 輸出的 pc 值並不具備參考性. 現在我們看看在同一個例子的輸出結果如何:

func main() {    for skip := 0; ; skip++ {        pc, file, line, ok := runtime.Caller(skip)        if !ok {            break        }        fmt.Printf("skip = %v, pc = %v, file = %v, line = %v\n", skip, pc, file, line)    }    // Output:    // skip = 0, pc = 4198456, file = caller.go, line = 10    // skip = 1, pc = 4280962, file = $(GOROOT)/src/pkg/runtime/proc.c, line = 220    // skip = 2, pc = 4290608, file = $(GOROOT)/src/pkg/runtime/proc.c, line = 1394    pc := make([]uintptr, 1024)    for skip := 0; ; skip++ {        n := runtime.Callers(skip, pc)        if n <= 0 {            break        }        fmt.Printf("skip = %v, pc = %v\n", skip, pc[:n])    }    // Output:    // skip = 0, pc = [4305334 4198635 4280962 4290608]    // skip = 1, pc = [4198635 4280962 4290608]    // skip = 2, pc = [4280962 4290608]    // skip = 3, pc = [4290608]}

比如輸出結果可以發現, 42809624290608 兩個 pc 值是相同的. 它們分別對應 runtime.mainruntime.goexit 函數.

runtime.Caller 輸出的 4198456runtime.Callers 輸出的 4198635 並不相同. 這是因為, 這兩個函數的調用位置並不相同, 因此導致了 pc 值也不完全相同.

最後就是 runtime.Callers 多輸出一個 4305334 值, 對應runtime.Callers內部的調用位置.

由於Go語言(Go1.2)採用分段堆棧, 因此不同的 pc 之間的大小關係並不明顯.

runtime.FuncForPC 的用途

函數的簽名如下:

func runtime.FuncForPC(pc uintptr) *runtime.Funcfunc (f *runtime.Func) FileLine(pc uintptr) (file string, line int)func (f *runtime.Func) Entry() uintptrfunc (f *runtime.Func) Name() string

其中 runtime.FuncForPC 返回包含給定 pc 地址的函數, 如果是無效 pc 則返回 nil .

runtime.Func.FileLine 返回與 pc 對應的源碼檔案名稱和行號. 安裝文檔的說明, 如果pc不在函數幀範圍內, 則結果是不確定的.

runtime.Func.Entry 對應函數的地址. runtime.Func.Name 返回該函數的名稱.

下面是 runtime.FuncForPC 的例子:

func main() {    for skip := 0; ; skip++ {        pc, _, _, ok := runtime.Caller(skip)        if !ok {            break        }        p := runtime.FuncForPC(pc)        file, line := p.FileLine(0)        fmt.Printf("skip = %v, pc = %v\n", skip, pc)        fmt.Printf("  file = %v, line = %d\n", file, line)        fmt.Printf("  entry = %v\n", p.Entry())        fmt.Printf("  name = %v\n", p.Name())    }    // Output:    // skip = 0, pc = 4198456    //   file = caller.go, line = 8    //   entry = 4198400    //   name = main.main    // skip = 1, pc = 4282882    //   file = $(GOROOT)/src/pkg/runtime/proc.c, line = 179    //   entry = 4282576    //   name = runtime.main    // skip = 2, pc = 4292528    //   file = $(GOROOT)/src/pkg/runtime/proc.c, line = 1394    //   entry = 4292528    //   name = runtime.goexit    pc := make([]uintptr, 1024)    for skip := 0; ; skip++ {        n := runtime.Callers(skip, pc)        if n <= 0 {            break        }        fmt.Printf("skip = %v, pc = %v\n", skip, pc[:n])        for j := 0; j < n; j++ {            p := runtime.FuncForPC(pc[j])            file, line := p.FileLine(0)            fmt.Printf("  skip = %v, pc = %v\n", skip, pc[j])            fmt.Printf("    file = %v, line = %d\n", file, line)            fmt.Printf("    entry = %v\n", p.Entry())            fmt.Printf("    name = %v\n", p.Name())        }        break    }    // Output:    // skip = 0, pc = [4307254 4198586 4282882 4292528]    //   skip = 0, pc = 4307254    //     file = $(GOROOT)/src/pkg/runtime/runtime.c, line = 315    //     entry = 4307168    //     name = runtime.Callers    //   skip = 0, pc = 4198586    //     file = caller.go, line = 8    //     entry = 4198400    //     name = main.main    //   skip = 0, pc = 4282882    //     file = $(GOROOT)/src/pkg/runtime/proc.c, line = 179    //     entry = 4282576    //     name = runtime.main    //   skip = 0, pc = 4292528    //     file = $(GOROOT)/src/pkg/runtime/proc.c, line = 1394    //     entry = 4292528    //     name = runtime.goexit}

根據測試, 如果是無效 pc (比如0), runtime.Func.FileLine 一般會輸出當前函數的開始行號. 不過在實踐中, 一般會用 runtime.Caller 擷取檔案名稱和行號資訊, runtime.Func.FileLine 很少用到(如何獨立擷取pc參數?).

定製的 CallerName 函數

基於前面的幾個函數, 我們可以方便的定製一個 CallerName 函數. 函數 CallerName 返回調用者的函數名/檔案名稱/行號等方便使用的資訊.

函數實現如下:

func CallerName(skip int) (name, file string, line int, ok bool) {    var pc uintptr    if pc, file, line, ok = runtime.Caller(skip + 1); !ok {        return    }    name = runtime.FuncForPC(pc).Name()    return}

其中在執行 runtime.Caller 調用時, 參數 skip + 1 用於抵消 CallerName 函數自身的調用.

下面是基於 CallerName 的輸出例子:

func main() {    for skip := 0; ; skip++ {        name, file, line, ok := CallerName(skip)        if !ok {            break        }        fmt.Printf("skip = %v\n", skip)        fmt.Printf("  file = %v, line = %d\n", file, line)        fmt.Printf("  name = %v\n", name)    }    // Output:    // skip = 0    //   file = caller.go, line = 19    //   name = main.main    // skip = 1    //   file = $(GOROOT)/src/pkg/runtime/proc.c, line = 220    //   name = runtime.main    // skip = 2    //   file = $(GOROOT)/src/pkg/runtime/proc.c, line = 1394    //   name = runtime.goexit}

這樣就可以方便的輸出函數調用者的資訊了.

Go語言中函數的類型

在Go語言中, 除了語言定義的普通函數調用外, 還有閉包函數/init函數/全域變數初始化等不同的函數調用類型.

為了便於測試不同類型的函數調用, 我們封裝一個 PrintCallerName 函數. 該函數用於輸出調用者的資訊.

func PrintCallerName(skip int, comment string) bool {    name, file, line, ok := CallerName(skip + 1)    if !ok {        return false    }    fmt.Printf("skip = %v, comment = %s\n", skip, comment)    fmt.Printf("  file = %v, line = %d\n", file, line)    fmt.Printf("  name = %v\n", name)    return true}

然後編寫以下的測試代碼(函數閉包調用/全域變數初始化/init函數等):

var a = PrintCallerName(0, "main.a")var b = PrintCallerName(0, "main.b")func init() {    a = PrintCallerName(0, "main.init.a")}func init() {    b = PrintCallerName(0, "main.init.b")    func() {        b = PrintCallerName(0, "main.init.b[1]")    }()}func main() {    a = PrintCallerName(0, "main.main.a")    b = PrintCallerName(0, "main.main.b")    func() {        b = PrintCallerName(0, "main.main.b[1]")        func() {            b = PrintCallerName(0, "main.main.b[1][1]")        }()        b = PrintCallerName(0, "main.main.b[2]")    }()}

輸出結果如下:

// Output:// skip = 0, comment = main.a//   file = caller.go, line = 8//   name = main.init// skip = 0, comment = main.b//   file = caller.go, line = 9//   name = main.init// skip = 0, comment = main.init.a//   file = caller.go, line = 12//   name = main.init·1// skip = 0, comment = main.init.b//   file = caller.go, line = 16//   name = main.init·2// skip = 0, comment = main.init.b[1]//   file = caller.go, line = 18//   name = main.func·001// skip = 0, comment = main.main.a//   file = caller.go, line = 23//   name = main.main// skip = 0, comment = main.main.b//   file = caller.go, line = 24//   name = main.main// skip = 0, comment = main.main.b[1]//   file = caller.go, line = 26//   name = main.func·003// skip = 0, comment = main.main.b[1][1]//   file = caller.go, line = 28//   name = main.func·002// skip = 0, comment = main.main.b[2]//   file = caller.go, line = 30//   name = main.func·003

觀察輸出結果, 可以發現以下幾個規律:

  • 全域變數的初始化調用者為 main.init 函數
  • 自訂的 init 函數有一個數字尾碼, 根據出現的順序進編號. 比如 main.init·1main.init·2 等.
  • 閉包函數採用 main.func·001 格式命名, 安裝閉包定義結束的位置順序進編號.

比如以下全域變數的初始化調用者為 main.init 函數:

var a = PrintCallerName(0, "main.a")var b = PrintCallerName(0, "main.b")

以下兩個 init 函數根據出現順序分別對應 main.init·1main.init·2 :

func init() { // main.init·1    //}func init() { // main.init·2    //}

以下三個閉包根據定義結束順序分別為 001 / 002 / 003 :

func init() {    func(){        //    }() // main.func·001}func main() {    func() {        func(){            //        }() // main.func·002    }() // main.func·003}

因為, 這些特殊函數調用方式的存在, 我們需要進一步完善 CallerName 函數.

改進的 CallerName 函數

兩類特殊的調用是 init 類函數調用 和 閉包函數調用.

改進後的 CallerName 函數對 init 類函數調用者統一處理為 init 函數. 將閉包函數調用這處理為調用者的函數名.

// caller types:// runtime.goexit// runtime.main// main.init// main.init·1// main.main// main.func·001// code.google.com/p/gettext-go/gettext.TestCallerName// ...func CallerName(skip int) (name, file string, line int, ok bool) {    var (        reInit    = regexp.MustCompile(`init·\d+$`) // main.init·1        reClosure = regexp.MustCompile(`func·\d+$`) // main.func·001    )    for {        var pc uintptr        if pc, file, line, ok = runtime.Caller(skip + 1); !ok {            return        }        name = runtime.FuncForPC(pc).Name()        if reInit.MatchString(name) {            name = reInit.ReplaceAllString(name, "init")            return        }        if reClosure.MatchString(name) {            skip++            continue        }        return    }    return}

處理的思路:

  1. 如果是 init 類型的函數調用(匹配Regex"init·\d+$"), 直接作為 init 函數範返回
  2. 如果是 func 閉包類型(匹配Regex"func·\d+$"), 跳過當前棧幀, 繼續遞迴處理
  3. 返回普通的函數調用類型

CallerName 函數的不足之處

有以下的代碼:

func init() {    var _ = myInit("1")}func main() {    var _ = myInit("2")}var myInit = func(name string) {    b = PrintCallerName(0, name + ":main.myInit.b")}

myInit 為一個全域變數, 被賦值為一個閉包函數. 然後在 initmain 函數分別調用 myInit 這個閉包函數輸出的結果
會因為調用環境的不同而有差異.

從直觀上看, myInit閉包函數在執行時, 最好輸出 main.myInit 函數名. 但是 main.myInit 只是一個綁定到閉包函數的變數, 而閉包的真正名字是 main.func·???. 在運行時是無法得到 main.myInit 這個名字的.

因此在 gettext-go 中內部用的 callerName 函數採用將 main.func·??? 統一處理為 main.func 的, 然後作為 gettext.Gettext 翻譯函數的上下文.

gettext-go 的 callerName 函數實現在這裡: caller.go. 測試檔案在這裡: caller_test.go.

不同Go程式啟動流程

基於函數調用者資訊可以很容易的驗證各種環境的程式啟動流程.

我們需要建立一個獨立的 caller 目錄, 裡面有三個測試代碼.

caller/main.go 主程式:

package mainimport (    "fmt"    "regexp"    "runtime")func main() {    _ = PrintCallerName(0, "main.main._")}func PrintCallerName(skip int, comment string) bool {    // 實現和前面的例子相同}func CallerName(skip int) (name, file string, line int, ok bool) {    // 實現和前面的例子相同}

caller/main_test.go 主程式的測試檔案(同在一個main包):

package mainimport (    "fmt"    "testing")func TestPrintCallerName(t *testing.T) {    for skip := 0; ; skip++ {        name, file, line, ok := CallerName(skip)        if !ok {            break        }        fmt.Printf("skip = %v, name = %v, file = %v, line = %v\n", skip, name, file, line)    }    t.Fail()}

caller/example_test.go 主程式的包的調用者(在新的main_test包):

package main_testimport (    myMain "."    "fmt")func Example() {    for skip := 0; ; skip++ {        name, file, line, ok := myMain.CallerName(skip)        if !ok {            break        }        fmt.Printf("skip = %v, name = %v, file = %v, line = %v\n", skip, name, file, line)    }    // Output: ?}

然後進入 caller 目錄, 運行 go run test 可以得到以下的輸出結果:

skip = 0, name = caller.TestPrintCallerName, file = caller/main_test.go, line = 10skip = 1, name = testing.tRunner, file = $(GOROOT)/src/pkg/testing/testing.go, line = 391skip = 2, name = runtime.goexit, file = $(GOROOT)/src/pkg/runtime/proc.c, line = 1394--- FAIL: TestPrintCallerName (0.00 seconds)--- FAIL: Example (2.0001ms)got:skip = 0, name = caller_test.Example, file = caller/example_test.go, line = 10skip = 1, name = testing.runExample, file = $(GOROOT)/src/pkg/testing/example.go, line = 98skip = 2, name = testing.RunExamples, file = $(GOROOT)/src/pkg/testing/example.go, line = 36skip = 3, name = testing.Main, file = $(GOROOT)/src/pkg/testing/testing.go, line = 404skip = 4, name = main.main, file = $(TEMP)/go-build365033523/caller/_test/_testmain.go, line = 51skip = 5, name = runtime.main, file = $(GOROOT)/src/pkg/runtime/proc.c, line = 220skip = 6, name = runtime.goexit, file = $(GOROOT)/src/pkg/runtime/proc.c, line = 1394want:?FAILexit status 1FAIL    caller        0.254s

分析輸出資料我們可以發現, 測試代碼和例子代碼的啟動流程和普通的程式流程都不太一樣.

測試代碼的啟動流程:

  1. runtime.goexit 還是入口
  2. 但是 runtime.goexit 不在調用 runtime.main 函數, 而是調用 testing.tRunner 函數
  3. testing.tRunner 函數由 go test 命令產生, 用於執行各個測試函數

例子代碼的啟動流程:

  1. runtime.goexit 還是入口
  2. 然後 runtime.goexit 調用 runtime.main 函數
  3. 最終 runtime.main 調用go test 命令產生的 main.main 函數, 在 _test/_testmain.go 檔案
  4. 然後調用 testing.Main, 改函數執行各個例子函數

另外, 從這個例子我們可以發現, 我們自己寫的 main.main 函數所在的 main 包也可以被其他包匯入. 但是其他包匯入之後的 main 包裡的 main 函數就不再是main.main 函數了. 因此, 程式的入口也就不是自己寫的 main.main 函數了.

總結

Go語言 runtime 包的 runtime.Caller / runtime.Callers / runtime.FuncForPC 等函數雖然看起來比較簡單, 但是功能卻非常強大.

這幾個函數不僅可以解決一些實際的工程問題(比如 gettext-go 中用於擷取翻譯的上下文資訊), 而且非常適合用於調試和分析各種Go程式的運行時資訊.

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.