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