大家好,我是煎魚。
有一次事故現場,在緊急恢復後,他正在排查程式碼,查了好一會。我回頭一看,這錯誤提醒很明顯就是致命錯誤,較好定位。
但此時,他竟然在查 panic-recover 是不是哪裡漏了,我表示大受震驚...
今天就由煎魚給大家分享一下錯誤型別有哪幾種,又在什麼場景下會觸發。
錯誤型別
error
第一種是 Go 中最標準的 error 錯誤,其真身是一個 interface{}。
如下:
type error interface {
Error() string
}
在日常工程中,我們只需要建立任意結構體,實現了 Error 方法,就可以認為是 error 錯誤型別。
如下:
type errorString struct {
s string
}
func (e *errorString) Error() string {
return e.s
}
在外部呼叫標準庫 API,一般如下:
f, err := os.Open("filename.ext")
if err != nil {
log.Fatal(err)
}
// do something with the open *File f
我們會約定最後一個引數為 error 型別,一般常見於第二個引數,可以有個約定俗成的習慣。
panic
第二種是 Go 中的異常處理 panic,能夠產生異常錯誤,結合 panic+recover 可以扭轉程式的執行狀態。
如下:
package main
import "os"
func main() {
panic("a problem")
_, err := os.Create("/tmp/file")
if err != nil {
panic(err)
}
}
輸出結果:
$ go run panic.go
panic: a problem
goroutine 1 [running]:
main.main()
/.../panic.go:12 +0x47
...
exit status 2
如果沒有使用 recover 作為捕獲,就會導致程式中斷。也因此經常被人誤以為程式中斷,就 100% 是 panic 導致的。
這是一個誤區。
throw
第三種是 Go 初學者經常踩坑,也不知道的錯誤型別,那就是致命錯誤 throw。
這個錯誤型別,在使用者側是沒法主動呼叫的,均為 Go 底層自行呼叫的,像是大家常見的 map 併發讀寫,就是由此觸發。
其原始碼如下:
func throw(s string) {
systemstack(func() {
print("fatal error: ", s, "\n")
})
gp := getg()
if gp.m.throwing == 0 {
gp.m.throwing = 1
}
fatalthrow()
*(*int)(nil) = 0 // not reached
}
根據上述程式,會獲取當前 G 的例項,並設定其 M 的 throwing 狀態為 1。
狀態設定好後,會呼叫 fatalthrow
方法進行真正的 crash 相關操作:
func fatalthrow() {
pc := getcallerpc()
sp := getcallersp()
gp := getg()
systemstack(func() {
startpanic_m()
if dopanic_m(gp, pc, sp) {
crash()
}
exit(2)
})
*(*int)(nil) = 0 // not reached
}
主體邏輯是傳送 _SIGABRT
訊號量,最後呼叫 exit
方法退出,所以你會發現這是攔也攔不住的 “致命” 錯誤。
致命場景
為此,作為一名 “成熟” 的 Go 工程師,除了保障自己程式的健壯性外,我也在網上收集了一些致命的錯誤場景,分享給大家。
一起學習和規避這些致命場景,年底爭取拿個 A,不要背上 P0 事故。
併發讀寫 map
func foo() {
m := map[string]int{}
go func() {
for {
m["煎魚1"] = 1
}
}()
for {
_ = m["煎魚2"]
}
}
輸出結果:
fatal error: concurrent map read and map write
goroutine 1 [running]:
runtime.throw(0x1078103, 0x21)
...
堆疊記憶體耗盡
func foo() {
var f func(a [1000]int64)
f = func(a [1000]int64) {
f(a)
}
f([1000]int64{})
}
輸出結果:
runtime: goroutine stack exceeds 1000000000-byte limit
runtime: sp=0xc0200e1bf0 stack=[0xc0200e0000, 0xc0400e0000]
fatal error: stack overflow
runtime stack:
runtime.throw(0x1074ba3, 0xe)
/usr/local/Cellar/go/1.16.6/libexec/src/runtime/panic.go:1117 +0x72
runtime.newstack()
...
將 nil 函式作為 goroutine 啟動
func foo() {
var f func()
go f()
}
輸出結果:
fatal error: go of nil func value
goroutine 1 [running]:
main.foo()
...
goroutines 死鎖
func foo() {
select {}
}
輸出結果:
fatal error: all goroutines are asleep - deadlock!
goroutine 1 [select (no cases)]:
main.foo()
...
執行緒限制耗盡
如果你的 goroutines 被 IO 操作阻塞了,新的執行緒可能會被啟動來執行你的其他 goroutines。
Go 的最大的執行緒數是有預設限制的,如果達到了這個限制,你的應用程式就會崩潰。
會出現如下輸出結果:
fatal error: thread exhaustion
...
可以通過呼叫 runtime.SetMaxThreads
方法增大執行緒數,不過也需要考量是否程式有問題。
超出可用記憶體
如果你執行的操作,例如:下載大檔案等。導致應用程式佔用記憶體過大,程式上漲,導致 OOM。
會出現如下輸出結果:
fatal error: runtime: out of memory
...
建議處理掉一些程式,或者換新電腦了。
總結
在今天這篇文章中,我們介紹了 Go 語言的三種錯誤型別。其中針對大家最少見,但一碰到就很容易翻車的致命錯誤 fatal error 進行了介紹,給出了一些經典案例。
希望大家後續能夠規避,你有沒有遇到過其中的場景?
若有任何疑問歡迎評論區反饋和交流,最好的關係是互相成就,各位的點贊就是煎魚創作的最大動力,感謝支援。
文章持續更新,可以微信搜【腦子進煎魚了】閱讀,本文 GitHub github.com/eddycjy/blog 已收錄,學習 Go 語言可以看 Go 學習地圖和路線,歡迎 Star 催更。
參考
- Are all runtime errors recoverable in Go?