在程式開發時我們都會認為外部提供的介面或者資料都是不可信的。比如函數總是要檢查入參的正確性,在做單元測試的時候要把外部提供的介面給屏蔽掉等。之所以都會這麼做,主要還是很難保證自己還是其他人可以提供一個沒有任何缺陷的介面。既然介面是人寫的,那麼多少會有些考慮不到的地方,這時候介面在被調用的時候就有可能發生錯誤或者異常。這裡討論golang中的異常處理機制,其實就是panic和recover這兩個介面的運用,類似於C++中的try和catch。
1、golang中的panic
panic,中文解釋為恐慌。舉個例子,單代碼中出現這樣的語句的時候,相信所有開發人員在產品上線的時候都會恐慌:
var MakecoreData *int = nil *MakecoreData = 10000
如果不做任何處理的時候,這個golang出現如果跑到這裡的時候就會出現這樣的結果。
panic: runtime error: invalid memory address or nil pointer dereference[signal 0xc0000005 code=0x1 addr=0x0 pc=0x5d0676]
然後重新就退出了,這個結果其實跟C/C++中的core是一樣的。
當然這個例子可能不會有人會犯這種低級錯誤,但是如果這兩個語句中間加上一段複製的邏輯,且需要一定的條件觸發的話,那可能就很難發現了。
再來看看golang中panic的機制,當程式發生異常的時候,golang語言層級的會拋出一個panic,以上面例子來說,當代碼運行到*MakecoreData = 10000的時候,會拋出一個panic,函數就結束了,如果沒有做任何處理,那麼程式中對應的g就異常,程式的生命週期就結束了。同樣的,如果在代碼中直接調用panic也是一致的結果。例如直接調用:panic(“have a panic”)的話,運行結果為:
panic(“have a panic”),然後顯示對應的堆棧資訊。
2,golang中的recover
在golang當異常發生的時候,都會產生一個panic。而一個panic總會有一個recover與之對應。當沒有任何處理的時候,預設的recover行為的行為是將進程退出。recover會返回一個error類型的傳回值,記錄異常的情況。由於當異常產生的時候,產生異常的函數將不再執行,會立即退出。那recover就只能在defer中調用,而且是要在產生異常前的defer語句中調用。因為在golang中,只有執行到defer語句的時候才會將對應的函數插入到defer隊列中,如果defer語句在異常產生後才調用,該defer對應要執行的函數由於沒有插入到defer隊列而沒有被調用到。
下面將以幾個例子來說明:
1)recover不在defer中:
import ( "fmt" "net/http" "runtime")func main() { if err := recover(); err != nil { fmt.Println(err) } var MakecoreData *int = nil *MakecoreData = 10000 if err := recover(); err != nil { fmt.Println(err) } fmt.Println("hello world")}
從代碼來看,產生異常的代碼前後都調用了recover,但是從結果來看,運行結果跟前後不加recover是一致的。
2)在異常代碼後增加一個defer語句,語句中有recover,例子如下:
import ( "fmt" "net/http" "runtime")func main() { var MakecoreData *int = nil *MakecoreData = 10000 defer func() { if err := recover(); err != nil { fmt.Println(err) } }() fmt.Println("hello world")}
從結果來看,還是跟沒有加recover是一致的。
3、在發生異常前增加一個defer語句,語句中有recover。例子如下:
import ( "fmt" "net/http" "runtime")func main() { defer func() { if err := recover(); err != nil { fmt.Println(err) } }() var MakecoreData *int = nil *MakecoreData = 10000 fmt.Println("hello world")}
這段代碼中recover才會真正的被執行。這就跟前面分析的一種。當遇到異常的時候,發生異常的代碼後面的函數體將不會被執行。函數退出,這時候會執行defer隊列中註冊的函數,如果在這裡有recover操作。那麼異常就會被捕獲到。然後函數退出,不會導致程式結束。
3、使用golang異常機制,增強代碼的健壯性。
從golang的異常機制可以看出,異常處理recover最好是在介面的入口處就將其插入到對應defer隊列中。這樣在介面調用過程中,即使發生異常,程式依然可以提供服務。以web服務為例子來看看如何使用golang的異常機制增強代碼的健壯性:
1、無任何異常處理機制的web服務如下:
import ( "fmt" "log" "net/http")func HelloServer(w http.ResponseWriter, req *http.Request) { var MakecoreData *int = nil *MakecoreData = 10000 fmt.Fprintf(w, "hello world")}func main() { http.HandleFunc("/hello", HelloServer) err := http.ListenAndServe(":12345", nil) if err != nil { log.Fatal("ListenAndServe: ", err) }}
這個代碼很明顯,在執行業務代碼的時候會發生異常。異常產生的原因跟上面章節描述的一致。但是如果服務有多個頁面,不僅僅只有/hello的時,當用戶端調用localhost:12345/hello以後,會返回一個404返回碼。但是服務並不會掛掉。原因在於golang的http包中在接受一個請求的時候會有先在入口處調用recover來保證golang開發工程師在寫業務代碼的時候,如果發生了異常,不至於使整個服務都崩潰掉。
4、利用golang其他功能配合異常處理機制增強服務的健壯性。
利用golang的異常機制,可以是服務在有缺陷的情況下依然可以提供部分暫時沒有缺陷的服務。但是這樣是遠遠不夠的,因為發現缺陷後他們需要修複,修改後還要重新部署。
對於大部分服務來說,服務需要提供24*7的不間斷服務。那麼在解決方案中,需要做的更多了。比如:
1、定位異常點。
定位方法可以通過golang中runtime包擷取堆棧資訊及入參並儲存,可以知道程式在哪裡發生異常,這樣可以方便後續的維護。
2、採用主備服務
當確定某個服務存在缺陷的時候,採用主備,不接業務的伺服器升級可以減少由於更新服務帶來的業務中斷。
3、服務業務模組採用熱插拔外掛程式式
業務模組是服務中跑的最多的代碼。一般如果存在異常,那麼至少有90%以上出現在業務模組代碼上。如果採用外掛程式的形式,看在服務不停止的情況下更新模組代碼。