自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

五分鐘搞定 Golang 錯(cuò)誤處理

開發(fā) 后端
本文介紹了 Go 語言處理和返回報(bào)錯(cuò)的最佳實(shí)踐。恰當(dāng)?shù)腻e(cuò)誤處理可以幫助開發(fā)人員更好的理解并調(diào)試程序中的問題。

本文介紹了 Go 語言處理和返回報(bào)錯(cuò)的最佳實(shí)踐。恰當(dāng)?shù)腻e(cuò)誤處理可以幫助開發(fā)人員更好的理解并調(diào)試程序中的問題,報(bào)錯(cuò)信息應(yīng)該描述性的表達(dá)出錯(cuò)的原因,并且應(yīng)該使用錯(cuò)誤哨兵和 errors.Is 來更好的實(shí)現(xiàn)錯(cuò)誤處理和調(diào)試。

級別 1: if err != nil

這是最簡單的錯(cuò)誤返回方法,大多數(shù)人都熟悉這種模式。如果代碼調(diào)用了一個(gè)可能返回錯(cuò)誤的函數(shù),那么檢查錯(cuò)誤是否為 nil,如果不是,則返回報(bào)錯(cuò)。

import (
 "errors"
 "fmt"
)

func doSomething() (float64, error) {
 result, err := mayReturnError();
 if err != nil {
  return 0, err
 }
 return result, nil
}

這種方法的問題:

雖然這可能是最簡單也是最常用的方法,但存在一個(gè)主要問題:缺乏上下文。如果代碼的調(diào)用棧比較深,就沒法知道是哪個(gè)函數(shù)報(bào)錯(cuò)。

想象一下,在某個(gè)調(diào)用棧中,函數(shù) A() 調(diào)用 B(),B() 調(diào)用 C(),C() 返回一個(gè)類似下面這樣的錯(cuò)誤:

package main

import (
 "errors"
 "fmt"
)

func A(x int) (int, error) {
 result, err := B(x)
 if err != nil {
  return 0, err
 }
 return result * 3, nil
}

func B(x int) (int, error) {
 result, err := C(x)
 if err != nil {
  return 0, err
 }
 return result + 2, nil
}

func C(x int) (int, error) {
 if x < 0 {
  return 0, errors.New("negative value not allowed")
 }
 return x * x, nil
}

func main() {
 // Call function A with invalid input
 result, err := A(-2)
 if err == nil {
  fmt.Println("Result:", result)
 } else {
  fmt.Println("Error:", err)
 }
}

如果運(yùn)行該程序,將輸出以下內(nèi)容:

Error: negative value not allowed

我們無法通過報(bào)錯(cuò)信息得知調(diào)用棧的哪個(gè)位置出錯(cuò),而不得不在代碼編輯器中打開程序,搜索特定錯(cuò)誤字符串,才能找到報(bào)錯(cuò)的源頭。

級別 2:封裝報(bào)錯(cuò)

為了給錯(cuò)誤添加上下文,我們用 fmt.Errorf 對錯(cuò)誤進(jìn)行包裝。

package main

import (
 "errors"
 "fmt"
)

func A(x int) (int, error) {
 result, err := B(x)
 if err != nil {
  return 0, fmt.Errorf("A: %w", err)
 }
 return result * 3, nil
}

func B(x int) (int, error) {
 result, err := C(x)
 if err != nil {
  return 0, fmt.Errorf("B: %w", err)
 }
 return result + 2, nil
}

func C(x int) (int, error) {
 if x < 0 {
  return 0, fmt.Errorf("C: %w", errors.New("negative value not allowed"))
 }
 return x * x, nil
}

func main() {
 // Call function A with invalid input
 result, err := A(-2)
 if err == nil {
  fmt.Println("Result:", result)
 } else {
  fmt.Println("Error:", err)
 }
}

運(yùn)行這個(gè)程序,會得到以下輸出結(jié)果:

Error: A: B: C: negative value not allowed

這樣就能知道調(diào)用棧。

但仍然存在問題。

這種方法的問題:

我們現(xiàn)在知道哪里報(bào)錯(cuò),但仍然不知道出了什么問題。

級別 3:描述性錯(cuò)誤

這個(gè)錯(cuò)誤描述得不夠清楚。為了說明這一點(diǎn),需要稍微復(fù)雜一點(diǎn)的例子。

import (
 "errors"
 "fmt"
)

func DoSomething() (int, error) {
 result, err := DoSomethingElseWithTwoSteps()
 if err != nil {
  return 0, fmt.Errorf("DoSomething: %w", err)
 }
 return result * 3, nil
}

func DoSomethingElseWithTwoSteps() (int, error) {
 stepOne, err := StepOne()
 if err != nil {
  return 0, fmt.Errorf("DoSomethingElseWithTwoSteps:%w", err)
 }

 stepTwo, err := StepTwo()
 if err != nil {
  return 0, fmt.Errorf("DoSomethingElseWithTwoSteps: %w", err)
 }

 return stepOne + StepTwo, nil
}

在本例中,沒法通過報(bào)錯(cuò)知道是哪個(gè)操作失敗了,不管是 StepOne 還是 StepTwo,都會收到同樣的錯(cuò)誤提示:Error:DoSomething: DoSomethingElseWithTwoSteps:UnderlyingError。

要解決這個(gè)問題,需要補(bǔ)充上下文,說明具體出了什么問題。

import (
 "errors"
 "fmt"
)

func DoSomething() (int, error) {
 result, err := DoSomethingElseWithTwoSteps()
 if err != nil {
  return 0, fmt.Errorf("DoSomething: %w", err)
 }
 return result * 3, nil
}

func DoSomethingElseWithTwoSteps() (int, error) {
 stepOne, err := StepOne()
 if err != nil {
  return 0, fmt.Errorf("DoSomethingElseWithTwoSteps: StepOne: %w", err)
 }

 stepTwo, err := StepTwo()
 if err != nil {
  return 0, fmt.Errorf("DoSomethingElseWithTwoSteps: StepTwo: %w", err)
 }

 return stepOne + StepTwo, nil
}

因此,如果 StepOne 失敗,就會收到錯(cuò)誤信息:DoSomething: DoSomethingElseWithTwoSteps:StepOne failed: UnderlyingError。

這種方法的問題:

  • 這些報(bào)錯(cuò)通過函數(shù)名來輸出調(diào)用棧,但并不能表達(dá)錯(cuò)誤的性質(zhì),錯(cuò)誤應(yīng)該是描述性的。
  • HTTP 狀態(tài)代碼就是個(gè)很好的例子。如果收到 404,就說明試圖獲取的資源不存在。

級別 4:錯(cuò)誤哨兵(Error Sentinels)

錯(cuò)誤哨兵是可以重復(fù)使用的預(yù)定義錯(cuò)誤常量。

函數(shù)失敗的原因有很多,但我喜歡將其大致分為 4 類。未找到錯(cuò)誤(Not Found Error)、已存在錯(cuò)誤(Already Exists Error)、先決條件失敗錯(cuò)誤(Failed Precondition Error)和內(nèi)部錯(cuò)誤(Internal Error),靈感來自 gRPC 狀態(tài)碼[2]。下面用一句話來解釋每種類型。

  • Not Found Error(未找到錯(cuò)誤):調(diào)用者想要的資源不存在。例如:已刪除的文章。
  • Already Exists Error(已存在錯(cuò)誤):調(diào)用者創(chuàng)建的資源已存在。例如:同名組織。
  • Failed Precondition Error(前提條件失敗錯(cuò)誤):調(diào)用者要執(zhí)行的操作不符合執(zhí)行條件或處于不良狀態(tài)。例如:嘗試從余額為 0 的賬戶中扣款。
  • Internal Error(內(nèi)部錯(cuò)誤):不屬于上述類別的任何其他錯(cuò)誤都屬于內(nèi)部錯(cuò)誤。

僅有這些錯(cuò)誤類型還不夠,必須讓調(diào)用者知道這是哪種錯(cuò)誤,可以通過錯(cuò)誤哨兵和 errors.Is 來實(shí)現(xiàn)。

假設(shè)有一個(gè)人們可以獲取和更新錢包余額的 REST API,我們看看如何在從數(shù)據(jù)庫獲取錢包時(shí)使用錯(cuò)誤哨兵。

import (
 "fmt"
 "net/http"
 "errors"
)

// These are error sentinels
var (
  WalletDoesNotExistErr = errors.New("Wallet does not exist") //Type of Not Found Error
  CouldNotGetWalletErr = errors.New("Could not get Wallet") //Type of Internal Error
)

func getWalletFromDB(id int) (int, error) {
 // Dummy implementation: simulate retrieving a wallet from a database
 balance, err := db.get(id)

 if err != nil {
  if balance == nil {
    return 0, fmt.Errorf("%w: Wallet(id:%s) does not exist: %w", WalletDoesNotExistErr, id, err)
  } else {
    return 0, return fmt.Errorf("%w: could not get Wallet(id:%s) from db: %w", CouldNotGetWalletErr, id, err)
  }
 }

 return *balance, nil
}

通過下面的 REST 處理程序,可以看到錯(cuò)誤哨兵是怎么用的。

func getWalletBalance() {
 wallet, err := getWalletFromDB(id)

 if errors.Is(err, WalletDoesNotExistErr) {
  // return 404
 } else if errors.Is(err, CouldNotGetWalletErr) {
  // return 500
 }
}

再看另一個(gè)用戶更新余額的例子。

import (
 "fmt"
 "net/http"
 "errors"
)

var (
  WalletDoesNotExistErr = errors.New("Wallet does not exist") //Type of Not Found Error
  CouldNotDebitWalletErr = errors.New("Could not debit Wallet") //Type of Internal Error
  InsiffucientWalletBalanceErr = errors.New("Insufficient balance in Wallet") //Type of Failed Precondition Error
)

func debitWalletInDB(id int, amount int) error {
 // Dummy implementation: simulate retrieving a wallet from a database
 balance, err := db.get(id)

 if err != nil {
  if balance == nil {
    return fmt.Errorf("%w: Wallet(id:%s) does not exist: %w", WalletDoesNotExistErr, id, err)
  } else {
    return fmt.Errorf("%w: could not get Wallet(id:%s) from db: %w", CouldNotDebitWalletErr, id, err)
  }
 }

 if *balance <= 0 {
   return 0, fmt.Errorf("%w: Wallet(id:%s) balance is 0", InsiffucientWalletBalanceErr, id)
 }

 updatedBalance := *balance - amount
 
 // Dummy implementation: simulate updating a wallet into a database
 err := db.update(id, updatedBalance)

 if err != nil {
   return fmt.Errorf("%w: could not update Wallet(id:%s) from db: %w", CouldNotDebitWalletErr, id, err)
 }

 return nil
}

利用哨兵編寫更好的錯(cuò)誤信息:

我喜歡用以下兩種方式來格式化錯(cuò)誤信息。

  • fmt.Errorf("%w: description: %w", Sentinel, err)
  • fmt.Errorf("%w: description", Sentinel)

這樣可以確保錯(cuò)誤能說明問題,解釋出錯(cuò)的現(xiàn)象和根本原因。

這一點(diǎn)很重要,因?yàn)閺纳厦娴睦又锌梢钥闯?,同一類型的錯(cuò)誤可能是由兩個(gè)不同的潛在問題造成的。因此,描述可以幫助我們準(zhǔn)確找出出錯(cuò)原因。

補(bǔ)充內(nèi)容:如何記錄錯(cuò)誤

不需要記錄所有錯(cuò)誤,為什么?

Error: C: negative value not allowed
Error: B: C: negative value not allowed
Error: A: B: C: negative value not allowed

相反,應(yīng)該只記錄 "被處理" 的錯(cuò)誤。所謂的 "被處理" 的錯(cuò)誤,是指調(diào)用者在收到報(bào)錯(cuò)后,可以對錯(cuò)誤進(jìn)行處理并繼續(xù)執(zhí)行,而不是僅僅返回錯(cuò)誤。

最好的例子還是 REST 處理程序。如果 REST 處理程序收到錯(cuò)誤,可以查看錯(cuò)誤類型,然后發(fā)送帶有狀態(tài)碼的響應(yīng),并停止傳播錯(cuò)誤。

func getWalletBalance() {
 wallet, err := getWalletFromDB(id)

 if err != nil {
  fmt.Printf("%w", err)
 }

 if errors.Is(err, WalletDoesNotExistErr) {
  // return 404
 } else if errors.Is(err, CouldNotGetWalletErr) {
  // return 500
 }
}

參考資料:

  • [1] Conquering Errors in Go: A Guide to Returning and Handling errors: https://blog.rideapp.in/conquering-errors-in-go-a-guide-to-returns-and-handling-a13885905433
  • [2] gRPC Status Codes: https://grpc.github.io/grpc/core/md_doc_statuscodes.html
責(zé)任編輯:趙寧寧 來源: DeepNoMind
相關(guān)推薦

2021-12-01 06:50:50

Docker底層原理

2025-01-21 07:39:04

Linux堆內(nèi)存Golang

2025-01-20 08:50:00

2017-09-27 11:00:50

LinuxBash使用技巧

2023-10-28 16:30:19

Golang開發(fā)

2020-02-21 19:54:09

HTTPS 配置手把手教

2015-12-03 14:10:26

systemd容器Linux

2017-12-20 09:42:39

PythonNginx日志

2022-12-13 10:05:27

定時(shí)任務(wù)任務(wù)調(diào)度操作系統(tǒng)

2023-04-04 09:13:15

2020-12-07 09:01:58

冪等系統(tǒng)f(f(x)) =f(

2024-12-04 16:12:31

2009-11-16 10:53:30

Oracle Hint

2024-12-11 07:00:00

面向?qū)ο?/a>代碼

2025-03-13 06:22:59

2020-06-16 08:47:53

磁盤

2009-11-26 11:19:52

NIS服務(wù)器

2011-05-26 09:03:17

JSONjavascript

2010-03-05 17:28:08

2020-08-20 10:16:56

Golang錯(cuò)誤處理數(shù)據(jù)
點(diǎn)贊
收藏

51CTO技術(shù)棧公眾號