写点什么

5 分钟搞定 Golang 错误处理

作者:俞凡
  • 2025-03-12
    上海
  • 本文字数:4190 字

    阅读完需:约 14 分钟

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


级别 1: if err != nil

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


import ( "errors" "fmt")
func doSomething() (float64, error) { result, err := mayReturnError(); if err != nil { return 0, err } return result, nil}
复制代码
这种方法的问题

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


想象一下,在某个调用栈中,函数 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) }}
复制代码


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


Error: negative value not allowed
复制代码


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

级别 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) }}
复制代码


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


Error: A: B: C: negative value not allowed
复制代码


这样就能知道调用栈。


但仍然存在问题。

这种方法的问题

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

级别 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}
复制代码


在本例中,没法通过报错知道是哪个操作失败了,不管是 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}
复制代码


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

这种方法的问题

这些报错通过函数名来输出调用栈,但并不能表达错误的性质,错误应该是描述性的。


HTTP 状态代码就是个很好的例子。如果收到 404,就说明试图获取的资源不存在。

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

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


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


Not Found Error(未找到错误):调用者想要的资源不存在。例如:已删除的文章。


Already Exists Error(已存在错误):调用者创建的资源已存在。例如:同名组织。


Failed Precondition Error(前提条件失败错误):调用者要执行的操作不符合执行条件或处于不良状态。例如:尝试从余额为 0 的账户中扣款。


Internal Error(内部错误):不属于上述类别的任何其他错误都属于内部错误。


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


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


import ( "fmt" "net/http" "errors")
// These are error sentinelsvar ( 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 处理程序,可以看到错误哨兵是怎么用的。


func getWalletBalance() { wallet, err := getWalletFromDB(id)
if errors.Is(err, WalletDoesNotExistErr) { // return 404 } else if errors.Is(err, CouldNotGetWalletErr) { // return 500 }}
复制代码


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


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}
复制代码
利用哨兵编写更好的错误信息

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


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

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


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


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

补充内容:如何记录错误

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


Error: C: negative value not allowedError: B: C: negative value not allowedError: A: B: C: negative value not allowed
复制代码


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


最好的例子还是 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 }}
复制代码




你好,我是俞凡,在 Motorola 做过研发,现在在 Mavenir 做技术工作,对通信、网络、后端架构、云原生、DevOps、CICD、区块链、AI 等技术始终保持着浓厚的兴趣,平时喜欢阅读、思考,相信持续学习、终身成长,欢迎一起交流学习。为了方便大家以后能第一时间看到文章,请朋友们关注公众号"DeepNoMind",并设个星标吧,如果能一键三连(转发、点赞、在看),则能给我带来更多的支持和动力,激励我持续写下去,和大家共同成长进步!

发布于: 刚刚阅读数: 3
用户头像

俞凡

关注

公众号:DeepNoMind 2017-10-18 加入

俞凡,Mavenir Systems研发总监,关注高可用架构、高性能服务、5G、人工智能、区块链、DevOps、Agile等。公众号:DeepNoMind

评论

发布
暂无评论
5 分钟搞定 Golang 错误处理_golang_俞凡_InfoQ写作社区