Golang巧用defer進行錯誤處理的方法

NO IMAGE

本文主要跟大家介紹了Golang巧用defer進行錯誤處理的相關內容,分享出來供大家參考學習,下面來看看詳細的介紹:

問題引入

毫無疑問,錯誤處理是程式的重要組成部分,有效且優雅的處理錯誤是大多數程式設計師的追求。很多程式設計師都有C/C 的程式設計背景,Golang的程式設計師也不例外,他們處理錯誤有意無意的帶著C/C 的烙印。

我們看看下面的例子,就有一種似曾相識的趕腳,程式碼如下:


func deferDemo() error {
err := createResource1()
if err != nil {
return ERR_CREATE_RESOURCE1_FAILED
}
err = createResource2()
if err != nil {
destroyResource1()
return ERR_CREATE_RESOURCE2_FAILED
}
err = createResource3()
if err != nil {
destroyResource1()
destroyResource2()
return ERR_CREATE_RESOURCE3_FAILED
}
err = createResource4()
if err != nil {
destroyResource1()
destroyResource2()
destroyResource3()
return ERR_CREATE_RESOURCE4_FAILED
}
return nil
}

從程式碼的實現中可以看出:在一個函式中,當建立新資源失敗時,則要清理所有前面已經建立成功的資源,這使得函式中有了重複程式碼的壞味道,比如destroyResource1函式呼叫了3次,destroyResource2函式呼叫了2次。

重構一:一個defer 多個flag

Golang提供了一個很好用的關鍵字defer,當包含defer的函式執行完畢時(不管是通過return的正常結束,還是由於panic導致的異常結束),defer語句才被呼叫。

考慮到這一點,我們嘗試將所有資源在defer語句中統一清理。由於函式返回時,不知道是否需要清理以及清理那些資源,所以要增加多個flag。

重構後的程式碼如下所示:


func deferDemo() error {
flag := false
flag1 := false
flag2 := false
flag3 := false
defer func() {
if !flag {
if flag3 {
destroyResource3()
}
if flag2 {
destroyResource2()
}
if flag1 {
destroyResource1()
}
}
}()
err := createResource1()
if err != nil {
return ERR_CREATE_RESOURCE1_FAILED
}
flag1 = true
err = createResource2()
if err != nil {
return ERR_CREATE_RESOURCE2_FAILED
}
flag2 = true
err = createResource3()
if err != nil {
return ERR_CREATE_RESOURCE3_FAILED
}
flag3 = true
err = createResource4()
if err != nil {
return ERR_CREATE_RESOURCE4_FAILED
}
flag = true
return nil
}

從重構後的程式碼可以看出,雖然消除了重複,但是引入了太多的flag:

flag表示函式是否執行成功,即flag為true時表示函式執行成功,否則表示函式執行失敗;在defer語句中,只有flag為false時才需要統一清理資源
flagi表示第i個資源是否建立成功,即flagi為true時表示第i個資源建立成功,否則表示第i個資源建立失敗;在defer語句中,只有flagi為true時才需要清理第i個資源

顯然,這不是我們想要的

重構二:多個defer

看過linux原始碼的同學都知道,在核心程式碼中,很多地方都通過goto語句來集中處理錯誤,非常優雅。

我們用這種方法將重構前的程式碼用C語言寫一下,程式碼如下所示:


ErrCode deferDemo()
{
ErrCode err = createResource1();
if (err != ERR_SUCC)
{
goto err_1;
}
err = createResource2();
if (err != ERR_SUCC)
{
goto err_2;
}
err = createResource3();
if (err != ERR_SUCC)
{
goto err_3;
}
err = createResource4();
if (err != ERR_SUCC)
{
goto err_4;
}
return ERR_SUCC;
err_4:
destroyResource3();
err_3:
destroyResource2();
err_2:
destroyResource1();
err_1:
return ERR_FAIL;
}

沒有重複,沒有flag,錯誤處理也很優雅,感覺很爽,那以前在C/C 編碼規範中禁止使用goto語句的規則確實有點過,呵呵…

從重構後的C程式碼中可以看出,create操作和destroy操作的順序類似入棧和出棧的順序:

伴隨著create操作,destroy操作逐個入棧,順序為1,2,3
出棧時是destroy操作,順序為3,2,1

於是我們又想到了defer語句:當Golang的程式碼執行時,如果遇到defer語句,則壓入堆疊,當函式返回時,會按照後進先出的順序呼叫defer語句。

我們看一個例子,程式碼如下所示:


func main() {
defer fmt.Println(1)
defer fmt.Println(2)
defer fmt.Println(3)
}

執行後,日誌如下所示:


3
2
1

然而,有堆疊特性還不夠,因為伴隨著create操作,destroy操作入棧是有條件的:

如果create操作失敗,則直接返回,那麼defer語句沒有執行,導致destroy操作沒有入棧
如果create操作成功,則defer語句得到執行,destroy操作完成入棧

可見,destroy操作的入棧條件是create操作成功,但是destroy操作並不是一定執行,只有當某個create操作失敗(“err != nil”)時,前面入棧的destory操作才需要執行,所以err的值也需要入棧。然而,destroy操作入棧時”err == nil” ,於是問題就變成:當err的值在後面變成非nil時,應該同步修改堆疊中的err值,即堆疊中傳遞的是引用或指標而不是值。

當err的引用或指標和destroy操作都需要入棧時,defer後面必須是一個閉包呼叫。我們知道,對於閉包的引數是值傳遞,而對於外部變數卻是引用傳遞。為了簡單優雅起見,我們將err不通過引數的指標傳遞,而通過外部變數的引用傳遞。

我們根據這個結論重構一下程式碼,如下所示:


func deferDemo() error {
err := createResource1()
if err != nil {
return ERR_CREATE_RESOURCE1_FAILED
}
defer func() {
if err != nil {
destroyResource1()
}
}()
err = createResource2()
if err != nil {
return ERR_CREATE_RESOURCE2_FAILED
}
defer func() {
if err != nil {
destroyResource2()
}
}()
err = createResource3()
if err != nil {
return ERR_CREATE_RESOURCE3_FAILED
}
defer func() {
if err != nil {
destroyResource3()
}
}()
err = createResource4()
if err != nil {
return ERR_CREATE_RESOURCE4_FAILED
}
return nil
}

本次重構消除了程式碼的壞味道,不由的感嘆一句:”升級了,我的哥!“

總結

本文通過巧用defer,有效且優雅的處理了錯誤,該技巧應該被所有的Golang程式設計師掌握並大量使用。

好了,以上就是這篇文章的全部內容了,希望本文的內容對大家的學習或者工作能帶來一定的幫助,如果有疑問大家可以留言交流,謝謝大家對指令碼之家的支援。

您可能感興趣的文章:

GO語言延遲函式defer用法分析總結Go語言中defer的使用和注意要點GO語言Defer用法例項分析golang中defer的使用規則詳解golang中defer的關鍵特性示例詳解Go語言中的延遲函式defer示例詳解