青海省高等級公路建設(shè)管理局網(wǎng)站阿里云盤資源搜索引擎
引言
程序遇到的錯誤大致分為兩類:程序員預(yù)料到的錯誤和程序員沒有預(yù)料到的錯誤。我們在前兩篇關(guān)于[錯誤處理]的文章中介紹的error
接口主要處理我們在編寫Go程序時預(yù)期的錯誤。error
接口甚至允許我們承認(rèn)函數(shù)調(diào)用發(fā)生錯誤的罕見可能性,因此我們可以在這些情況下進(jìn)行適當(dāng)?shù)捻憫?yīng)。
異常屬于第二類錯誤,是程序員沒有預(yù)料到的。這些不可預(yù)見的錯誤會導(dǎo)致程序自動終止并退出正在運行的Go程序。常見的錯誤往往會造成異常。在本教程中,我們將研究Go中常見操作可能產(chǎn)生嚴(yán)重錯誤的幾種方式,我們還將了解避免這些嚴(yán)重錯誤的方法。我們還將使用[defer
]語句和recover
函數(shù)來捕獲錯誤,以免它們意外地終止我們正在運行的Go程序。
理解異常
Go中的某些操作會自動返回異常并停止程序。常見的操作包括超出[array]容量的索引、執(zhí)行類型斷言、調(diào)用nil指針的方法、錯誤地使用互斥量以及嘗試使用閉合通道。這些情況大多是由編程時犯的錯誤導(dǎo)致的,而編譯器在編譯程序時無法檢測到這些錯誤。
由于異常包含對解決問題有用的細(xì)節(jié),開發(fā)人員通常將異常用作在程序開發(fā)過程中犯了錯誤的指示。
出界異常
當(dāng)您試圖訪問超出切片長度或數(shù)組容量的索引時,Go運行時將生成一個異常。
下面的例子犯了一個常見的錯誤,即試圖使用內(nèi)置函數(shù)len
返回的切片長度來訪問切片的最后一個元素。試著運行這段代碼,看看為什么會產(chǎn)生異常:
package mainimport ("fmt"
)func main() {names := []string{"lobster","sea urchin","sea cucumber",}fmt.Println("My favorite sea creature is:", names[len(names)])
}
Outputpanic: runtime error: index out of range [3] with length 3goroutine 1 [running]:
main.main()/tmp/sandbox879828148/prog.go:13 +0x20
異常的輸出名稱提供了一個提示:panic: runtime error: index out of range
。我們制作了一個有三個海洋生物的切片。然后,我們嘗試使用內(nèi)置函數(shù)len
將切片的長度作為索引來獲取切片的最后一個元素。別忘了,切片和數(shù)組是從0開始的;所以這個切片的第一個元素是0,最后一個元素的索引是2
。由于我們試圖在第三個索引3
處訪問切片,因此切片中沒有可以返回的元素,因為它超出了切片的邊界。運行時別無選擇,只能終止和退出,因為我們要求它做一些不可能的事情。Go也無法在編譯期間證明此代碼將嘗試執(zhí)行此操作,因此編譯器無法捕獲此操作。
還要注意,后續(xù)的代碼沒有運行。這是因為異常是一個會完全停止Go程序執(zhí)行的事件。生成的消息包含多種有助于診斷異常原因的信息。
異常的剖析
嚴(yán)重錯誤由指示嚴(yán)重錯誤原因的消息和堆棧跟蹤組成,后者可以幫助您定位代碼中產(chǎn)生嚴(yán)重錯誤的位置。
任何異常的第一部分都是信息。它總是以字符串panic:
開始,后面跟著一個根據(jù)異常原因而變化的字符串。上一個練習(xí)中的異常語句包含如下消息:
panic: runtime error: index out of range [3] with length 3
panic:
前綴后面的字符串runtime error:
告訴我們,異常是由語言運行時生成的。這個錯誤告訴我們,我們試圖使用的索引[3]
超出了切片長度3
的范圍。
此消息之后是堆棧跟蹤。堆棧跟蹤形成了一個map,我們可以根據(jù)它準(zhǔn)確定位生成異常時正在執(zhí)行的代碼行,以及之前的代碼如何調(diào)用該代碼。
goroutine 1 [running]:
main.main()/tmp/sandbox879828148/prog.go:13 +0x20
前面例子中的堆棧跟蹤顯示,我們的程序從第13行文件/tmp/sandbox879828148/prog.go
中生成了異常。它還告訴我們,此異常是在main
包中的main()
函數(shù)中生成的。
堆棧跟蹤被分成多個獨立的塊——一個用于程序中的每個goroutine。每個Go程序的執(zhí)行都是由一個或多個goroutines完成的,這些goroutines可以獨立和同時執(zhí)行Go代碼的部分內(nèi)容。每個塊都以goroutine X [state]:
開頭。header給出了goroutine的ID號以及發(fā)生異常時它所處的狀態(tài)。在header之后,堆棧跟蹤顯示了發(fā)生嚴(yán)重錯誤時程序正在執(zhí)行的函數(shù),以及該函數(shù)執(zhí)行的文件名和行號。
前面例子中的異常是由對切片的越界訪問產(chǎn)生的。當(dāng)對未設(shè)置的指針調(diào)用方法時,也可能產(chǎn)生異常。
Nil接收器
Go編程語言有指針,指向運行時存在于計算機內(nèi)存中的某種類型的特定實例。指針可以假定值為nil
,表示它們不指向任何東西。當(dāng)我們試圖在一個為nil
的指針上調(diào)用方法時,Go運行時將生成一個異常。類似地,接口類型的變量在被方法調(diào)用時也會產(chǎn)生錯誤。要查看在這些情況下產(chǎn)生的嚴(yán)重錯誤,請嘗試以下示例:
package mainimport ("fmt"
)type Shark struct {Name string
}func (s *Shark) SayHello() {fmt.Println("Hi! My name is", s.Name)
}func main() {s := &Shark{"Sammy"}s = nils.SayHello()
}
Outputpanic: runtime error: invalid memory address or nil pointer dereference
[signal SIGSEGV: segmentation violation code=0xffffffff addr=0x0 pc=0xdfebagoroutine 1 [running]:
main.(*Shark).SayHello(...)/tmp/sandbox160713813/prog.go:12
main.main()/tmp/sandbox160713813/prog.go:18 +0x1a
在這個例子中,我們定義了一個名為Shark
的結(jié)構(gòu)體。Shark
在它的指針接收器上定義了一個名為SayHello
的方法,當(dāng)被調(diào)用時,它會向標(biāo)準(zhǔn)輸出打印一條問候語。在我們的main
函數(shù)體內(nèi),我們創(chuàng)建了這個Shark
結(jié)構(gòu)體的一個新實例,并使用&
操作符請求一個指向它的指針。這個指針被賦值給s
變量。然后我們使用s = nil
語句將s
變量重新賦值為nil
。最后,我們嘗試在變量s
上調(diào)用SayHello
方法。我們沒有收到來自Sammy的友好消息,而是收到了一個警告,表示我們試圖訪問一個無效的內(nèi)存地址。因為s
變量是nil
,當(dāng)調(diào)用SayHello
函數(shù)時,它嘗試訪問*Shark
類型的字段Name
。因為這是一個指針接收器,在這個例子中接收器是nil
,它無法解引nil
指針,所以出現(xiàn)了問題。
雖然我們在這個例子中顯式地將s
設(shè)置為nil
,但實際上這種情況并不明顯。當(dāng)你看到涉及nil pointer dereference
的嚴(yán)重錯誤時,請確保你已經(jīng)正確地為任何你可能創(chuàng)建的指針變量賦值。
由nil指針產(chǎn)生的嚴(yán)重錯誤和越界訪問是運行時產(chǎn)生的兩種常見嚴(yán)重錯誤。也可以使用內(nèi)置函數(shù)手動生成異常。
使用內(nèi)置函數(shù)panic
我們也可以使用內(nèi)置函數(shù)panic
來生成我們自己的異常。它接受一個字符串作為參數(shù),即異常將產(chǎn)生的消息。通常,這條消息比重寫代碼返回錯誤要簡潔。此外,我們可以在我們自己的包中使用它,以表明開發(fā)人員在使用我們的包的代碼時可能犯了錯誤。只要有可能,最佳實踐是嘗試向我們的包的使用者返回error
值。
運行以下代碼,查看從另一個函數(shù)調(diào)用的函數(shù)中生成的異常:
package mainfunc main() {foo()
}func foo() {panic("oh no!")
}
Outputpanic: oh no!goroutine 1 [running]:
main.foo(...)/tmp/sandbox494710869/prog.go:8
main.main()/tmp/sandbox494710869/prog.go:4 +0x40
在這里,我們定義了一個函數(shù)foo
,它使用字符串"oh no!"
調(diào)用內(nèi)置的panic
。這個函數(shù)被我們的main
函數(shù)調(diào)用。注意輸出的信息panic: oh no!
,堆棧跟蹤顯示了一個單獨的goroutine,其中有兩行代碼:一行用于main()
函數(shù),另一行用于我們的foo()
函數(shù)。
我們已經(jīng)看到,異常似乎會在產(chǎn)生它們的地方終止程序。當(dāng)需要關(guān)閉開放資源時,這可能會產(chǎn)生問題。Go提供了一種始終執(zhí)行某些代碼的機制,即使在出現(xiàn)緊急情況時也是如此。
延遲函數(shù)
你的程序可能有必須正確清理的資源,即使在運行時處理異常時也是如此。Go允許您推遲函數(shù)調(diào)用的執(zhí)行,直到調(diào)用它的函數(shù)完成執(zhí)行。延遲函數(shù)即使在緊急情況下也會運行,它被用作一種安全機制,以防范緊急情況帶來的混亂。通過像往常一樣調(diào)用函數(shù),然后用defer
關(guān)鍵字前綴整個語句來延遲函數(shù),就像defer sayHello()
一樣。運行下面的例子,看看在發(fā)生異常事件時如何打印消息:
package mainimport "fmt"func main() {defer func() {fmt.Println("hello from the deferred function!")}()panic("oh no!")
}
Outputhello from the deferred function!
panic: oh no!goroutine 1 [running]:
main.main()/Users/gopherguides/learn/src/github.com/gopherguides/learn//handle-panics/src/main.go:10 +0x55
在這個例子中的main
函數(shù)中,我們首先defer
了對一個匿名函數(shù)的調(diào)用,該匿名函數(shù)會打印出消息"hello from the deferred function!"
。然后main
函數(shù)立即使用panic
函數(shù)產(chǎn)生一個異常。在這個程序的輸出中,我們首先看到deferred函數(shù)被執(zhí)行了,并打印了它的消息。接下來是我們在main
中生成的異常。
延遲函數(shù)可以防止意外情況的發(fā)生。在延遲函數(shù)中,Go還為我們提供了使用另一個內(nèi)置函數(shù)阻止異常終止我們的Go程序的機會。
處理異常
異常只有一個恢復(fù)機制——內(nèi)置函數(shù)recover
。這個函數(shù)允許你在異常通過調(diào)用棧的過程中攔截它,防止它意外地終止你的程序。它有嚴(yán)格的使用規(guī)則,但在生產(chǎn)應(yīng)用程序中可能是非常寶貴的。
由于它是builtin
包的一部分,因此可以在不導(dǎo)入任何其他包的情況下調(diào)用recover
:
package mainimport ("fmt""log"
)func main() {divideByZero()fmt.Println("we survived dividing by zero!")}func divideByZero() {defer func() {if err := recover(); err != nil {log.Println("panic occurred:", err)}}()fmt.Println(divide(1, 0))
}func divide(a, b int) int {return a / b
}
Output2009/11/10 23:00:00 panic occurred: runtime error: integer divide by zero
we survived dividing by zero!
在這個例子中,我們的main
函數(shù)調(diào)用了我們定義的函數(shù)divideByZero
。在這個函數(shù)中,我們defer
調(diào)用一個匿名函數(shù),該函數(shù)負(fù)責(zé)處理在執(zhí)行divideByZero
時可能出現(xiàn)的任何錯誤。在這個延遲匿名函數(shù)中,我們調(diào)用recover
內(nèi)置函數(shù),并將它返回的錯誤賦值給一個變量。如果divideByZero
處于異常狀態(tài),這個error
值將被設(shè)置,否則它將是nil
。通過比較err
變量和nil
變量,我們可以檢測是否發(fā)生了異常,在這種情況下,我們使用log.Println
函數(shù)記錄異常,就像它是其他任何error
一樣。
在這個延遲匿名函數(shù)之后,我們調(diào)用另一個我們定義的函數(shù)divide
,并嘗試使用fmt.Println
打印它的結(jié)果。提供的參數(shù)將導(dǎo)致divide
執(zhí)行除數(shù)為0的運算,這將產(chǎn)生一個異常。
在這個示例的輸出中,我們首先看到恢復(fù)panic的匿名函數(shù)的日志消息,然后是消息 we survived dividing by zero!
。我們確實做到了這一點,感謝內(nèi)置函數(shù)recover
阻止了將終止我們的Go程序的災(zāi)難性異常。
recover()
返回的err
值正好是調(diào)用panic()
時提供的值。因此,當(dāng)異常沒有發(fā)生時,確保err
值僅為nil至關(guān)重要。
用recover
檢測異常
recover
函數(shù)依賴于錯誤的值來確定是否發(fā)生了嚴(yán)重錯誤。因為panic
函數(shù)的參數(shù)是一個空接口,所以它可以是任何類型。任何接口類型的0值,包括空接口,都是nil
。必須注意避免將nil
作為panic
的參數(shù),如下例所示:
package mainimport ("fmt""log"
)func main() {divideByZero()fmt.Println("we survived dividing by zero!")}func divideByZero() {defer func() {if err := recover(); err != nil {log.Println("panic occurred:", err)}}()fmt.Println(divide(1, 0))
}func divide(a, b int) int {if b == 0 {panic(nil)}return a / b
}```go```shell
Outputwe survived dividing by zero!
這個例子與前面的例子相同,涉及到recover
,只是做了一些細(xì)微的修改。divide
函數(shù)被修改為檢查它的除數(shù)b
是否等于0
。如果是,它將使用內(nèi)置的panic
并傳入nil
參數(shù)來生成一個異常。這一次的輸出不包括顯示發(fā)生了嚴(yán)重錯誤的日志消息,即使divide
創(chuàng)建了一個嚴(yán)重錯誤。這種靜默行為就是為什么確保panic
內(nèi)置函數(shù)的參數(shù)不是nil
非常重要的原因。
總結(jié)
我們已經(jīng)看到了Go中創(chuàng)建panic
的多種方式,以及如何使用內(nèi)置的recover
恢復(fù)它們。雖然您自己可能不需要使用panic
,但從panic中正確恢復(fù)是使Go應(yīng)用程序可用于生產(chǎn)的重要步驟。