在 Go 中處理錯誤

健壯的代碼需要對用戶的不正確輸入、網絡連接錯誤和磁盤錯誤等意外情況做出正確的反應。錯誤處理是識別程序處於異常狀態並且採取措施去記錄供後期調試診斷信息的過程。

相比於其他編程語言, 要求開發者使用專門的語法去處理錯誤, 在 Go 中將錯誤作爲 error(Go 中的一個接口類型) 類型的值, 並且和其他類型的值一樣作爲函數返回值的一部分返回。要處理 Go 中的錯誤, 我們必須檢查函數返回值中是否包含了錯誤信息, 並採取合適的措施去保護數據並告知用戶或者操作人員發生錯誤。

創建錯誤

在處理錯誤之前,我們需要先創建一些錯誤。標準庫提供了兩個內置函數來創建錯誤:errors.Newfmt.Errorf。這兩個函數都允許您指定一條自定義錯誤消息,這些信息可以向用戶展示具體錯誤信息的一部分。

errors.New 只提供了一個字符串類型的參數, 用戶在使用的時候可以自定義一個錯誤發生時具體需要展示的錯誤消息.

嘗試運行以下示例以查看由 errors.New 創建的錯誤並打印到標準輸出:

package main

import (
 "errors"
 "fmt"
)

func main() {
    // 使用 errors.New() 創建一個錯誤, 具體的錯誤消息是: barnacles
 err := errors.New("barnacles")

    // 將錯誤直接打印到標準錯誤輸出
 fmt.Println("Sammy says:", err)
}
# 這裏是控制檯的輸出
# Output
Sammy says: barnacles

我們使用標準庫的 errors.New 函數創建了具體的消息是 "barnacles" 的錯誤。這裏我們遵循了 Go 程序設計風格指南 使用小寫了表示錯誤消息。

最後,我們使用 fmt.Println 函數將我們的錯誤消息與"Sammy says:"相結合並且輸出到控制檯。

fmt.Errorf 函數允許用戶構建動態的錯誤消息。它的第一個參數是一個字符串,包含包含佔位符值的錯誤消息,例如字符串的 %s 和整數的%dfmt.Errorf 將這個格式化字符串後面的參數按順序插入到這些佔位符中:

package main

import (
 "fmt"
 "time"
)

func main() {
 // 使用 fmt.Errorf() 來構建動態錯誤信息
 // 錯誤內容是 error occurred at: %v
 // 其中 %v 的具體內容由 time.Now() 的具體返回值決定
 err := fmt.Errorf("error occurred at: %v", time.Now())

 // 將具體的錯誤信息 結合 `An error happened:` 打印到控制檯
 fmt.Println("An error happened:", err)
}
# 在控制檯中輸出錯誤信息
# Output
# 輸出內容中的: 2019-07-11 16:52:42.532621 -0400 EDT m=+0.000137103 是由 `time.Now()` 動態生成的
An error happened: error occurred at: 2019-07-11 16:52:42.532621 -0400 EDT m=+0.000137103

我們使用 fmt.Errorf 函數來構建一個錯誤消息,該消息將包括當前時間。我們提供給 fmt.Errorf 的格式字符串包含 %v 格式指令,該指令告訴 fmt.Errorf 使用默認格式爲格式化字符串後提供的第一個參數。這個參數由標準庫的 time.Now 函數提供的當前時間。與較早的示例類似,我們將錯誤消息與簡短前綴結合在一起,並使用 fmt.Println 函數將結果打印到標準輸出。

錯誤處理

一般來說, 你不會看到像上面一樣直接創建錯誤, 然後直接打印。實際上, 在出現問題時, 錯誤都是由從函數中創建並且返回這種情況更加普遍。調用者使用 if 語句判斷返回的錯誤是否爲 nil(error 非初始化的值) 來判斷錯誤是否存在。

下面這個示例包含了一個總是返回錯誤的函數, 需要特別留意的時儘管這裏的錯誤是由一個函數返回的, 當你在運行這個程序時, 它產生的輸出總是與前面的示例相同。在其他位置聲明錯誤不會改變錯誤的消息。

package main

import (
 "errors"
 "fmt"
)

// 定義一個名爲: boom 的函數, 返回值總是 errors.New("barnacles")
func boom() error {
 return errors.New("barnacles")
}

func main() {
 // 調用 boom() 函數, 並將返回值賦值給 err 變量
 err := boom()

 // 判斷 err 是否等於  nil 
 if err != nil {
  // 如果 err != nil 條件成立, 輸出內容, 然後返回 main.main 函數
  fmt.Println("An error occurred:", err)
  return
 }

 // 如果 err == nil 成立, 
 // 將會輸出下面這一句
 fmt.Println("Anchors away!")
}
# Output
An error occurred: barnacles

這裏我們先定義了一個名爲 boom() 的函數並且總是返回單個使用 errors.New 構造 error 的函數。然後, 我們通過 err := boom() 這行調用 boom() 並捕捉錯誤 (賦值給 err 變量即爲捕捉錯誤)。在賦值 error 之後, 我們使用 if err != nil 這個條件判斷語句來進行判斷錯誤是否存在。因爲 boom() 函數總是返回有效的 error 所以這裏的判斷條件永遠爲 true

但是情況並非總是如此 (值的是 boom() 函數總是返回有效的 error 變量), 所以, 最好有邏輯去處理錯誤不存在和錯誤存在這兩種情況。當錯誤存在時, 就像上面的示例中一樣, 我們使用 fmt.Println 和前面的前綴打印錯誤。最後我們使用 return 語句來跳過 fmt.Println("Anchors away!") 語句的執行, 因爲這個語句只有在 err == nil 時纔會執行。

注意: 在 Go 中主要採用上一個示例中的 if err != nil 來進行錯誤處理。函數運行到哪裏都有可能發生錯誤, 重要的是使用 if 語句來判斷錯誤是否發生。這樣, Go 代碼通常就具有第一個縮進級別的 快樂路徑 的邏輯, 並且所有的 "悲傷的路徑" 在第二個縮進。

if 語句有一個可選的賦值子句,可以用來幫助壓縮函數調用和錯誤處理。

運行下一個程序,查看與前面示例相同的輸出,但這一次使用複合 if 語句來減少一些重複的代碼:

package main

import (
 "errors"
 "fmt"
)

func boom() error {
 return errors.New("barnacles")
}

func main() {
 // 將 err 變量的賦值和判斷都壓縮在一個語句塊中執行
 if err := boom(); err != nil {
  fmt.Println("An error occurred:", err)
  return
 }
 fmt.Println("Anchors away!")
}
#Output
An error occurred: barnacles

和之前的示例一樣, 我們定義一個 boom() 總是返回錯誤的函數。我們將從 boom() 返回的錯誤賦值給 err 作爲 if 語句的一部分。在 if 語句的第二部分語句中, err 變量變得可用。我們檢查錯誤是否存在, 然後像以前一樣使用一個簡短的前綴字符串打印我們的錯誤。

在本節中,我們學習瞭如何處理只返回錯誤的函數。這些函數很常見,但是能夠處理可能返回多個值的函數的錯誤也很重要。

同時返回錯誤和多個值

返回單個值的函數通常是影響某些狀態更改的函數。比如將行數據插入到數據庫中。通常還會編寫這樣的函數: 如果成功則返回一個值, 如果失敗則返回一個潛在的錯誤。Go 允許函數返回多個結果, 可以用來同時返回一個值和一個錯誤類型。

爲了創建一個返回多個值的函數, 我們需要在函數簽名的括號中列出返回值類型。例如, 一個 capitalize 函數返回值類型是 stringerror, 那麼我們可以這麼聲明 func capitalize(name string)(string, error){}。其中 (string, error) 這一塊的語法是告訴 Go 的編譯器, 函數會按照 stringerror 這一順序返回值。

運行下面的程序並且查看函數返回的 stringerror:

package main

import (
 "errors"
 "fmt"
 "strings"
)

func capitalize(name string) (string, error) {
 if name == "" {
  return "", errors.New("no name provided")
 }
 return strings.ToTitle(name), nil
}

func main() {
 name, err := capitalize("sammy")
 if err != nil {
  fmt.Println("Could not capitalize:", err)
  return
 }

 fmt.Println("Capitalized name:", name)
}
# Output
Capitalized name: SAMMY

我們定義了 capitalize() 函數, 這個函數需要傳遞一個字符串作爲參數 (完成將字符串的轉爲大寫) 並返回字符串和錯誤。在 main() 函數中, 我們調用 capitalize(), 然後在 := 運算符的左邊將函數的返回值賦值給 nameerr 這兩個變量。之後, 我們執行 if err != nil 檢查錯誤, 如果存在錯誤, 使用 fmt.Prtintln 將錯誤信息打印到標準輸出。如果沒有錯誤, 輸出 Capitalized name: SAMMY

如果將 err := capitalize("sammy") 中的 "sammy" 更改爲爲空字符串 (""),你將收到 Could not capitalize: no name provided 這個錯誤。

當函數的調用者爲 name 參數提供一個空字符串時, capitalize 函數將返回錯誤。當 name 參數不是空字符串時,capledize() 調用 strings.ToTitle 函數將 name 參數轉爲大寫並返回爲 nil 的錯誤值。

這個例子遵循一些微妙的規約,這些規約是 Go 代碼的典型特徵,但 GO 編譯器並沒有強制執行。當函數返回多個值(包括錯誤)時,規約我們將 error 類型作爲最後一項。具有多個返回值的函數返回錯誤時,通常約定 GO 代碼還將每個不是 error 類型的值設置爲零值。比如字符串的零值空字符串,整數爲 0,一個用於結構類型的空結構,以及用 nil 表示接口和指針類型的零值。我們在有關 變量和常數的教程 中更詳細地介紹零值。

簡化重複的代碼

如果函數有多個返回值時,遵守這些約定可能會變得囉嗦。我們可以使用 匿名函數 來幫助減少重複的代碼。匿名函數是分配變量的過程。與我們在較早的示例中定義的函數相反,它們僅在你聲明它們的函數中可用 - 這使其非常適合用作可重複使用的 helper 邏輯代碼片段。

以下程序是修改了最後一個示例,返回值增加了一個類型, 包括大寫的名稱的長度。由於它具有三個值可以返回的值,因此如果沒有匿名函數來幫助我們,處理錯誤可能會變得麻煩:

package main

import (
 "errors"
 "fmt"
 "strings"
)

func capitalize(name string) (string, int, error) {
 handle := func(err error) (string, int, error) {
  return "", 0, err
 }

 if name == "" {
  return handle(errors.New("no name provided"))
 }

 return strings.ToTitle(name), len(name), nil
}

func main() {
 name, size, err := capitalize("sammy")
 if err != nil {
  fmt.Println("An error occurred:", err)
 }

 fmt.Printf("Capitalized name: %s, length: %d", name, size)
}
# Output
Capitalized name: SAMMY, length: 5

main() 中,我們現在可以從 capitalize() 函數中獲取轉爲大寫的 namesizeerr 這三個返回的參數。然後,我們檢查是否通過檢查錯誤變量不等於 nil。在嘗試使用 capitalize() 返回的任何其他值之前,這一點很重要,因爲匿名函數可以將它們設置爲零值。由於我們提供了字符串 "Sammy",因此沒有發生錯誤,因此我們打印出轉爲大寫之後的名稱及其長度。

再次,你可以嘗試將 "Sammy" 更改爲空字符串 ("") 以查看已打印的錯誤情況 (An error occurred: no name provided)。

capitalize 函數中,我們將 handle 變量定義爲匿名函數。它需要傳遞要給錯誤類型的參數,並以與 capitalize 函數的返回值相同的順序返回相同的值。handle 將這些值設置爲零值,並將其作爲最終返回值作爲參數傳遞的錯誤轉發。然後,使用 err 作爲 handle 的參數,就可以返回在 capitalize 中遇到的任何錯誤。

請記住,capitalize 必須一直返回三個值,因爲這就是我們定義函數的方式。有時我們不想處理函數可能返回的所有值。幸運的是,我們在賦值並如何使用這些值方面具有一定的靈活性。

處理多回報功能的錯誤

當函數返回許多值時,Go 要求我們將每個值分配給變量。在最後一個示例中,我們通過提供從 capitalize 函數返回的兩個值的名稱來做到這一點。這些名稱應通過逗號分隔,並出現在 := 操作符的左側。從 capitalize 返回的第一個值將分配給 name 變量,第二個值 (error) 將分配給 err 這個變量。有時,我們只對錯誤值感興趣。您可以丟棄使用特殊 _ 變量名稱返回功能的任何不需要值。

在以下程序中,我們修改了涉及大寫功能的第一個示例,以通過傳遞空字符串 ("") 來產生錯誤。嘗試運行此程序,以查看我們如何通過使用 _ 變量丟棄第一個返回的值來檢查錯誤:

package main

import (
 "errors"
 "fmt"
 "strings"
)

func capitalize(name string) (string, error) {
 if name == "" {
  return "", errors.New("no name provided")
 }
 return strings.ToTitle(name), nil
}

func main() {
 _, err := capitalize("")
 if err != nil {
  fmt.Println("Could not capitalize:", err)
  return
 }
 fmt.Println("Success!")
}
# Output
Could not capitalize: no name provided

這次在 main() 函數中,我們將 capitalize 的第一個返回值 (首先返回的字符串) 分配給下劃線變量 (_)。同時,我們分配了通過  capitalize 返回的 err 變量返回的錯誤。然後,我們通過 if err != nil 條件判斷錯誤是否存在。由於我們已經對一個空字符串進行了硬編碼,作爲在行中大寫的參數,_, err := capitalize(""),因此該條件始終將評估爲 true。這會產生輸出"Could not capitalize: no name provided",該輸出由 fmt.Println 函數在 if 語句的正文中打印出來。此後的返回將跳過 fmt.Println("Success!")

結論

我們已經看到了許多使用標準庫創建錯誤的方法,以及如何構建以慣用方式返回錯誤的函數。在本教程中,我們設法使用標準庫的 errors.Newfmt.Errorf 函數成功地創建了各種錯誤。在將來的教程中,我們將研究如何創建自己的自定義錯誤類型,以向用戶傳達更豐富的信息。

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/JWzqO6bkVJ8PP2rx1fpOXA