理解 Go 語言中的 panic 輸出

我的代碼有一個 bug。?

panic: runtime error: invalid memory address or nil pointer dereference
[signal SIGSEGV: segmentation violation code=0x1 addr=0x30 pc=0x751ba4]
goroutine 58 [running]:
github.com/joeshaw/example.UpdateResponse(0xad3c60, 0xc420257300, 0xc4201f4200, 0x16, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, ...)
        /go/src/github.com/joeshaw/example/resp.go:108 +0x144
github.com/joeshaw/example.PrefetchLoop(0xacfd60, 0xc420395480, 0x13a52453c000, 0xad3c60, 0xc420257300)
        /go/src/github.com/joeshaw/example/resp.go:82 +0xc00
created by main.runServer
        /go/src/github.com/joeshaw/example/cmd/server/server.go:100 +0x7e0

這個 panic 錯誤正如輸出的第一行所指示那樣,是由解引用一個 nil 指針造成的。由於 Go 在錯誤處理中的語法,相比於其它的語言,比如 C 或者 Java,這些類型的錯誤在 Go 中是不太常見的。 這種類型的錯誤在 Go 中比在其他語言 (如 C 或 Java) 中要少得多, 這得益於 Go 的錯誤處理方式。

如果一個函數執行失敗,那麼這個函數一定會返回一個 error 作爲它的最後一個返回值。調用者應該立即檢查該函數返回的錯誤。

// val is a pointer, err is an error interface value
val, err := somethingThatCouldFail()
if err != nil {
    // Deal with the error, probably pushing it up the call stack
    return err
}

// By convention, nearly all the time, val is guaranteed to not be
// nil here.

然而,這裏一定某處有一個 bug(譯註:指開頭的 panic),違反了這個隱式 API 的約定。

在我深入介紹之前,這裏有個附加說明:這(上述代碼)是與體系結構和操作系統有關的,我僅僅在 amd64 Linux 系統和 macOS 系統上運行。其它的系統運行結果應該會有所不同。

panic 錯誤輸出的第二行給出有關觸發這個 panic 的 UNIX 信號的信息:

[signal SIGSEGV: segmentation violation code=0x1 addr=0x30 pc=0x751ba4]

因爲一個 nil 指針的解引用而發生了段錯誤(SIGSEGV)。code 區映射到 UNIX 的 siginfo.si_cod 區,並且在 Linux 的 siginfo.h0x1 值是 SEGV_MAPERR(地址未映射到對象)。

addr 映射到 siginfo.si_addr,其值是 0x30,這並不是一個有效的內存地址。

pc 是程序計數器,我們可以使用它來找出程序崩潰的地方,但是我們通常沒必要這麼做,因爲一個 goroutine 跟蹤有如下信息。

goroutine 58 [running]:
github.com/joeshaw/example.UpdateResponse(0xad3c60, 0xc420257300, 0xc4201f4200, 0x16, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, ...)
        /go/src/github.com/joeshaw/example/resp.go:108 +0x144
github.com/joeshaw/example.PrefetchLoop(0xacfd60, 0xc420395480, 0x13a52453c000, 0xad3c60, 0xc420257300)
        /go/src/github.com/joeshaw/example/resp.go:82 +0xc00
created by main.runServer
        /go/src/github.com/joeshaw/example/cmd/server/server.go:100 +0x7e0

在這個深層次的棧幀中,第一個導致 panic 發生的(文件)會先列出。在這個例子中,是 resp.go 文件的 108 行。

在這個 goroutine 回溯信息裏,吸引我眼球的東西是函數 UpdateResponsePrefetchLoop 的參數, 因爲該數字與函數簽名不匹配。

func UpdateResponse(c Client, id string, version int, resp *Response, data []byte) error
func PrefetchLoop(ctx context.Context, interval time.Duration, c Client)

UpdateResponse 需要 5 個參數,但是 panic 顯示它攜帶超過 10 個參數。 PrefetchLoop 需要 3 個參數,但 panic 顯示它帶有 5 個參數。這樣會發生什麼呢?

爲了理解參數值,我們必須要了解一些關於 Go 底層類型的數據結構。RussCox 有兩篇很棒的博客,一篇關於 基本類型,結構體和指針,字符串和切片 ,另一篇關於 接口 ,它描述了這些在內存中是怎樣分佈的。對於 Go 程序員,這兩篇文章是必備讀物,但是概括起來是:

  • 字符串有兩個域 (一個指向字符串數據的指針和一個長度)
  • 切片有三個域 (一個指向底層數組的指針,一個長度,一個容量)
  • 接口有兩個域 (一個指向類型的指針和一個指向值的指針)

當 panic 發生時,我們看到在輸出中的參數值包括字符串、切片和接口的導出值。另外,函數的返回值會被添加到參數列表的末尾。

回到我們的 UpdateResponse 函數,Client 類型是一個接口,它帶有 2 個值。 id 是一個字符串,它有 2 個值(共 4 個)。version 是一個整型,帶有 1 個值(共 5 個值)。resp 是一個指針,帶有 1 個值(共 6 個)。data 是一個切片,帶有 3 個值(共 9 個值)。error 返回值是一個接口,所以又多 2 個值,從而總數到達 11 個。panic 輸出數目限制爲 10 個, 所以最後一個值在輸出中被截斷。

這是一個帶有註釋的 UpdateResponse 棧幀:

github.com/joeshaw/example.UpdateResponse(
    0xad3c60,      // c Client interface, type pointer
    0xc420257300,  // c Client interface, value pointer
    0xc4201f4200,  // id string, data pointer
    0x16,          // id string, length (0x16 = 22)
    0x1,           // version int (1)
    0x0,           // resp pointer (nil!)
    0x0,           // data slice, backing array pointer (nil)
    0x0,           // data slice, length (0)
    0x0,           // data slice, capacity (0)
    0x0,           // error interface (return value), type pointer
    ...            // truncated; would have been error interface value pointer
)

這有助於確認該消息來源的建議, 即 respnil,它被解引用了。

上移一個棧幀到 PrefetchLoop: ctx context.Context 是一個接口值, interval 是一個 time.Duration (它僅僅是一個 int64), Client 也是一個接口。

PrefetchLoop 註釋爲:

github.com/joeshaw/example.PrefetchLoop(
    0xacfd60,       // ctx context.Context interface, type pointer
    0xc420395480,   // ctx context.Context interface, value pointer
    0x13a52453c000, // interval time.Duration (6h0m)
    0xad3c60,       // c Client interface, type pointer
    0xc420257300,   // c Client interface, value pointer
)

正如我之前提過的,resp 本不應該是 nil,因爲這種情況只有在當返回 error 不是 nil 時纔會發生。罪魁禍首是在代碼中錯誤的使用了 github.com/pkg/errorsWrapf() 函數而不是 Errorf()

// Function returns (*Response, []byte, error)

if resp.StatusCode != http.StatusOK {
    return nil, nil, errors.Wrapf(err, "got status code %d fetching response %s", resp.StatusCode, url)
}

如果 Wrapf() 的第一個參數傳入爲 nil, 則它的返回值爲 nil。當這個 HTTP 狀態碼不是 http.StatusOK,這個函數將錯誤的返回 nil,nil,nil,因爲一個非 200 的狀態碼不是一個錯誤,因此 err 的值爲 nil。將 errors.Wrapf() 調用換成errors.Errorf() 可以修復這個 bug。

理解並且結合上下文語境中看 panic 輸出可以更容易的追蹤到錯誤!希望這些信息日後對你有用。

感謝 Peter Teichman, Damian Gryski, 和 Travis Bischel,他們幫助我分析 panic 的輸出參數列表。


via: https://joeshaw.org/understanding-go-panic-output/

作者:Joe Shaw 譯者:liuxinyu123 校對:polaris1119

本文由 GCTT 原創編譯,Go語言中文網 榮譽推出

更多Go語言知識,歡迎關注微信公衆號:Go語言中文網
在這裏插入圖片描述

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章