五分钟搞定 Golang 错误处理

开发 后端
本文介绍了 Go 语言处理和返回报错的最佳实践。恰当的错误处理可以帮助开发人员更好的理解并调试程序中的问题。

本文介绍了 Go 语言处理和返回报错的最佳实践。恰当的错误处理可以帮助开发人员更好的理解并调试程序中的问题,报错信息应该描述性的表达出错的原因,并且应该使用错误哨兵和 errors.Is 来更好的实现错误处理和调试。

级别 1: if err != nil

这是最简单的错误返回方法,大多数人都熟悉这种模式。如果代码调用了一个可能返回错误的函数,那么检查错误是否为 nil,如果不是,则返回报错。

import (
 "errors"
 "fmt"
)

func doSomething() (float64, error) {
 result, err := mayReturnError();
 if err != nil {
  return 0, err
 }
 return result, nil
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.

这种方法的问题:

虽然这可能是最简单也是最常用的方法,但存在一个主要问题:缺乏上下文。如果代码的调用栈比较深,就没法知道是哪个函数报错。

想象一下,在某个调用栈中,函数 A() 调用 B(),B() 调用 C(),C() 返回一个类似下面这样的错误:

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)
 }
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.
  • 33.
  • 34.
  • 35.
  • 36.
  • 37.
  • 38.
  • 39.

如果运行该程序,将输出以下内容:

Error: negative value not allowed
  • 1.

我们无法通过报错信息得知调用栈的哪个位置出错,而不得不在代码编辑器中打开程序,搜索特定错误字符串,才能找到报错的源头。

级别 2:封装报错

为了给错误添加上下文,我们用 fmt.Errorf 对错误进行包装。

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)
 }
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.
  • 33.
  • 34.
  • 35.
  • 36.
  • 37.
  • 38.
  • 39.

运行这个程序,会得到以下输出结果:

Error: A: B: C: negative value not allowed
  • 1.

这样就能知道调用栈。

但仍然存在问题。

这种方法的问题:

我们现在知道哪里报错,但仍然不知道出了什么问题。

级别 3:描述性错误

这个错误描述得不够清楚。为了说明这一点,需要稍微复杂一点的例子。

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
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.

在本例中,没法通过报错知道是哪个操作失败了,不管是 StepOne 还是 StepTwo,都会收到同样的错误提示:Error:DoSomething: DoSomethingElseWithTwoSteps:UnderlyingError。

要解决这个问题,需要补充上下文,说明具体出了什么问题。

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
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.

因此,如果 StepOne 失败,就会收到错误信息:DoSomething: DoSomethingElseWithTwoSteps:StepOne failed: UnderlyingError。

这种方法的问题:

  • 这些报错通过函数名来输出调用栈,但并不能表达错误的性质,错误应该是描述性的。
  • HTTP 状态代码就是个很好的例子。如果收到 404,就说明试图获取的资源不存在。

级别 4:错误哨兵(Error Sentinels)

错误哨兵是可以重复使用的预定义错误常量。

函数失败的原因有很多,但我喜欢将其大致分为 4 类。未找到错误(Not Found Error)、已存在错误(Already Exists Error)、先决条件失败错误(Failed Precondition Error)和内部错误(Internal Error),灵感来自 gRPC 状态码[2]。下面用一句话来解释每种类型。

  • Not Found Error(未找到错误):调用者想要的资源不存在。例如:已删除的文章。
  • Already Exists Error(已存在错误):调用者创建的资源已存在。例如:同名组织。
  • Failed Precondition Error(前提条件失败错误):调用者要执行的操作不符合执行条件或处于不良状态。例如:尝试从余额为 0 的账户中扣款。
  • Internal Error(内部错误):不属于上述类别的任何其他错误都属于内部错误。

仅有这些错误类型还不够,必须让调用者知道这是哪种错误,可以通过错误哨兵和 errors.Is 来实现。

假设有一个人们可以获取和更新钱包余额的 REST API,我们看看如何在从数据库获取钱包时使用错误哨兵。

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
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.

通过下面的 REST 处理程序,可以看到错误哨兵是怎么用的。

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

 if errors.Is(err, WalletDoesNotExistErr) {
  // return 404
 } else if errors.Is(err, CouldNotGetWalletErr) {
  // return 500
 }
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.

再看另一个用户更新余额的例子。

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
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.
  • 33.
  • 34.
  • 35.
  • 36.
  • 37.
  • 38.
  • 39.

利用哨兵编写更好的错误信息:

我喜欢用以下两种方式来格式化错误信息。

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

这样可以确保错误能说明问题,解释出错的现象和根本原因。

这一点很重要,因为从上面的例子中可以看出,同一类型的错误可能是由两个不同的潜在问题造成的。因此,描述可以帮助我们准确找出出错原因。

补充内容:如何记录错误

不需要记录所有错误,为什么?

Error: C: negative value not allowed
Error: B: C: negative value not allowed
Error: A: B: C: negative value not allowed
  • 1.
  • 2.
  • 3.

相反,应该只记录 "被处理" 的错误。所谓的 "被处理" 的错误,是指调用者在收到报错后,可以对错误进行处理并继续执行,而不是仅仅返回错误。

最好的例子还是 REST 处理程序。如果 REST 处理程序收到错误,可以查看错误类型,然后发送带有状态码的响应,并停止传播错误。

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.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.

参考资料:

  • [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
责任编辑:赵宁宁 来源: DeepNoMind
相关推荐

2025-01-20 08:50:00

2021-12-01 06:50:50

Docker底层原理

2025-01-21 07:39:04

Linux堆内存Golang

2017-09-27 11:00:50

LinuxBash使用技巧

2023-10-28 16:30:19

Golang开发

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

定时任务任务调度操作系统

2023-04-04 09:13:15

2020-12-07 09:01:58

幂等系统f(f(x)) =f(

2024-12-04 16:12:31

2024-12-11 07:00:00

面向对象代码

2009-11-16 10:53:30

Oracle Hint

2025-03-13 06:22:59

2020-06-16 08:47:53

磁盘

2009-11-26 11:19:52

NIS服务器

2011-05-26 09:03:17

JSONjavascript

2010-03-05 17:28:08

2020-08-20 10:16:56

Golang错误处理数据
点赞
收藏

51CTO技术栈公众号