我的代碼有一個 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.h
中 0x1
值是 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 回溯信息裏,吸引我眼球的東西是函數 UpdateResponse
和 PrefetchLoop
的參數, 因爲該數字與函數簽名不匹配。
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
)
這有助於確認該消息來源的建議, 即 resp
是 nil
,它被解引用了。
上移一個棧幀到 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/errors
的 Wrapf()
函數而不是 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
更多Go語言知識,歡迎關注微信公衆號:Go語言中文網