Golang错误处理的姿势
创始人
2024-05-28 16:29:56
0

背景

Golang在错误处理上是日常被大家吐槽的地方,我在开发也看到过很多做法,比较多的是在各个层级的逻辑代码中对错误的重复处理。
比如:有的代码会在每一层级上都判断错误并记录日志,从代码层面上看,没什么问题,貌似很严谨,但是如果查看日志的话会发现有一堆重复的信息,等到你真正去排查问题时反而会造成干扰。

一、了解error

error类型是一个内置的接口类型,该接口只规定了一个返回字符串值的Error方法。Golang通过error值来表示错误


// The error built-in interface type is the conventional interface for
// representing an error condition, with the nil value representing no error.
type error interface {Error() string
}

使用Golang在开发过程中经常会返回一个error值,调用者通过判断error值是否为nil来进行错误处理


num, err := strconv.Atoi("666")
if err != nil {fmt.Printf("could not convert to number: %v\n", err)return
}
fmt.Println("converted value:", num)

error为nil时表示成功;非nil的error表示出现错误

自定义错误实现error接口

我们经常会自定义符合自己需要的错误类型,切记要让这些自定义的错误类型实现error接口,这样的好处是:不用在调用方的代码中再引入额外的类型

例如,下面我们自定义了errorx类型,如果不实现error接口的话,调用方的代码中就会被errorx类型侵入:

package errorximport ("fmt""time"
)type errorx struct {Code      intCreatedAt time.TimeMsg       string
}func (e *errorx) Error() string {return fmt.Sprintf("at %v, %s, code %d",e.CreatedAt, e.Msg, e.Code)
}func run() error {return errorx{4002,time.Now(),"An error occurred",}
}func Do() {if err := run(); err != nil {fmt.Println(err)}
}

如果errorx不实现error接口的话,上面的run方法的返回值就要定义成errorx类型。然后调用方的代码中就要被errorx侵入,通过可导出类型的Errorx.Code == xxx 来判断到底是哪种具体的错误。

那调用方如何判断自定义的错误类型返回的error是哪种具体的错误呢? 方案有很多,比如:

  • 对外暴露检查错误的方法

errorx.Is(err, target error)
// ...
  • 对外暴漏自定义的错误常量,通过比较error值是否与错误常量相等进行判断,比如:gorm的查无数据(gorm.ErrRecordNotFound)、文件操作的结束符(io.EOF)…

if err != gorm.ErrRecordNotFound {return err
}
// ...

错误处理的常见问题

下面先来看一个简单例子,模拟出现错误的场景:


type Config struct {Username stringPassword int
}func JsonToMap(byteSlice []byte, data *map[string]string) error {err := json.Unmarshal(byteSlice, &data)if err != nil {log.Println("Failed to convert json to map:", err)return err}return nil
}func StructToMap(obj interface{}) (map[string]string, error) {byteSlice, err := json.Marshal(obj)if err != nil {log.Println("Failed to convert structure to json:", err)return nil, err}var data map[string]stringerr1 := JsonToMap(byteSlice, &data)if err1 != nil {log.Println("Failed to convert structure to map:", err1)return nil, err1}return data, nil
}func main () {result, err := StructToMap(errorx.Config{Username: "user",Password: 123456,})fmt.Println(result)fmt.Println(err)
}

上面例子中代码的错误处理出现了两个问题:

  • 1、底层函数JsonToMap出现错误后,除了向上一层返回错误外还向日志里记录了错误,上层调用者也做了同样的事情,记录日志然后把错误再返回给上层。

可能你们会觉得这样做没什么,但当你的项目不再是单个小项目单体,而是分布式、微服务的架构时,在联调/排查问题的时候,这绝对会让你裂开,即便是有链路追踪,也能让你人都麻了(趟过浑水来的[emo])。千万不要小瞧了log的作用!!!
在单体项目中当你把一个复杂功能的接口拆成多个逻辑方法去整合调用时,多个error往上层接力的时候也会出现这个情况(前提是你的接口不再是像之前写的那样一个function从头写到尾几百行…)。

所以在日志中出现一堆重复内容:


Failed to convert json to map: json: cannot unmarshal number into Go value of type string
Failed to convert structure to map: json: cannot unmarshal number into Go value of type string
...
  • 2、在代码的最顶层,虽然得到了原始错误,但没有相关的层级错误内容,换句话说就是没有把JsonToMap、StructToMap记录到log里的那些信息整合到错误中,最后再返回给上层,这样就不用每个地方都打印一次log。

针对以上两个问题的解决方案:在底层函数JsonToMap、StructToMap中给错误添加上下文信息,然后将错误整合返回给上层,由最上层方法最后统一处理错误。

最简单的可以直接使用fmt.Errorf方法,给错误添加注释信息。


type Config struct {Username stringPassword int
}func JsonToMap(byteSlice []byte, data *map[string]string) error {err := json.Unmarshal(byteSlice, &data)if err != nil {return fmt.Errorf("failed to convert json to map: %v", err)}return nil
}func StructToMap(obj interface{}) (map[string]string, error) {byteSlice, err := json.Marshal(obj)if err != nil {return nil, fmt.Errorf("failed to convert structure to json: %v", err)}var data map[string]stringerr1 := JsonToMap(byteSlice, &data)if err1 != nil {return nil, fmt.Errorf("failed to convert structure to map: %v", err1)}return data, nil
}

fmt.Errorf方法,只是给error添加注释标记信息,如果还想给error附加上下文信息,也就是error的调用栈,可以引用github.com/pkg/errors包,它提供了很多错误包装的方法,通过调用栈可以清楚的找到错误的位置,十分强大。


// 附加新的注释信息
// WithMessage annotates err with a new message.
func WithMessage(err error, message string) error// 附加调用的堆栈信息
// WithStack annotates err with a stack trace at the point WithStack was called.
func WithStack(err error) error// 同时附加调用栈信息和注释信息
// Wrap returns an error annotating err with a stack trace
// at the point Wrap is called, and the supplied message.
func Wrap(err error, message string) error

以上是其中的一些错误包装方法,有包装就有对应的拆包解包


// Cause方法会返回包装错误对应的最根本的错误,通俗说就是会递归地进行解包
// Cause returns the underlying cause of the error, if possible.
func Cause(err error) error {

下面是一个使用"github.com/pkg/errors"包的简单demo:

package errorxfunc ReadFile(path string) ([]byte, error) {f, err := os.Open(path)if err != nil {return nil, errors.Wrap(err, "open failed")}defer f.Close()buf, err := ioutil.ReadAll(f)if err != nil {return nil, errors.Wrap(err, "read failed")}return buf, nil
}// 模拟读取配置
func ReadConfig() ([]byte, error) {_ = os.Setenv("FILE_PATH", "./test.yaml")config, err := ReadFile(os.Getenv("FILE_PATH"))return config, errors.WithMessage(err, "could not read config")
}
package mainfunc main() {_, err := errorx.ReadConfig()if err != nil {fmt.Printf("original error Type: %T \noriginal error: %v\n", errors.Cause(err), errors.Cause(err))fmt.Printf("stack trace:\n%+v\n", err)os.Exit(1)}
}

当你的配置文件test.yaml不存在时,展示错误如下:


➜  test go run err.go
original error Type: *fs.PathError                           # 原始错误的类型
original error: open ./test.yaml: no such file or directory  # 原始错误原因
stack trace:                                                 # 调用栈信息
open ./test.yaml: no such file or directory
open failed
demo/errorx.ReadFile/Users/sheliutao/www/golang/src/test/errorx/errorx.go:67
demo/errorx.ReadConfig/Users/sheliutao/www/golang/src/test/errorx/errorx.go:79
main.main/Users/sheliutao/www/golang/src/test/err.go:41
runtime.main/usr/local/go/src/runtime/proc.go:250
runtime.goexit/usr/local/go/src/runtime/asm_amd64.s:1571
could not read config                                       # 附加注释信息
exit status 1

通过引入"github.com/pkg/errors"包既能给错误附加调用栈信息,又能保留原始错误的展示,通过Cause方法可以还原到最根本引发错误的详细信息和文件位置。

注意:Golang官方不推荐使用panic和recover去做错误处理。只有当程序不能继续运行的时候,才应该使用panic和recover机制。注意不要滥用panic和recover,可能会导致性能问题。应该尽可能地使用error去做错误处理,而不是使用panic和recover。

总结

最后总结下错误处理原则:

  • 只在逻辑的最外层处理(拆解)一次错误,底层只返回error错误。
  • 底层除了返回error外,要对原始错误进行包装,增加注释信息、调用栈等这些有利于排查的上下文信息。

我是六涛sheliutao,文章编写总结不易,转载注明出处,喜欢本篇文章的小伙伴欢迎点赞、关注,有问题可以评论区留言或者私信我,相互交流!!!

参考

  • https://pkg.go.dev/github.com/pkg/errors#section-readme

相关内容

热门资讯

安卓系统能跑win吗,探索跨平... 你有没有想过,你的安卓手机里能不能装上Windows系统呢?这听起来是不是有点像科幻电影里的情节?别...
安卓车载系统蓝牙设置,畅享智能... 你有没有发现,现在开车的时候,手机和车载系统之间的互动越来越频繁了呢?这不,今天就来给你详细说说安卓...
奥利奥安卓系统,探索新一代智能... 你有没有想过,一块小小的奥利奥饼干竟然能和强大的安卓系统扯上关系?没错,今天就要来聊聊这个跨界组合,...
微信使用安卓系统,功能解析与操... 你有没有发现,现在用微信的人越来越多了呢?尤其是安卓系统的用户,简直就像潮水一样涌来。今天,就让我带...
体验最新原生安卓系统,极致体验... 你有没有想过,手机系统就像是我们生活的调味品,有时候换一种口味,生活都会变得有趣起来呢?最近,我体验...
安卓系统能玩原神,尽享奇幻冒险... 你有没有想过,在安卓系统上也能畅玩《原神》这样的热门游戏呢?没错,就是那个画面精美、角色丰富、玩法多...
安卓写手机银行系统,基于安卓平... 你有没有想过,手机银行系统在我们日常生活中扮演了多么重要的角色呢?每天刷刷手机,就能轻松管理账户,转...
僵尸之夜恐怖安卓系统,揭秘恐怖... 僵尸之夜,恐怖安卓系统来袭!想象一个寂静的夜晚,你正沉浸在美梦中,突然,一阵诡异的铃声打破了夜的宁静...
谷歌框架和安卓系统,构建智能移... 你有没有想过,为什么你的手机那么聪明,能帮你找到路线,还能帮你拍出美美的照片呢?这都要归功于一个超级...
安卓系统和oppo系统哪个流畅... 你有没有想过,手机系统哪个更流畅呢?安卓系统和OPPO系统,这两个名字听起来就让人心动。今天,咱们就...
安卓怎么用微软系统,利用微软系... 你是不是也和我一样,对安卓手机上的微软系统充满了好奇?想象那熟悉的Windows界面在你的安卓手机上...
安卓系统如何安装nfc,安卓系... 你有没有想过,用手机刷公交卡、支付账单,是不是比掏出钱包来得酷炫多了?这就得归功于NFC技术啦!今天...
ios系统可以转安卓,跨平台应... 你有没有想过,你的iPhone手机里的那些宝贝应用,能不能搬到安卓手机上继续使用呢?没错,今天就要来...
iOSapp移植到安卓系统,i... 你有没有想过,那些在iOS上让你爱不释手的app,是不是也能在安卓系统上大放异彩呢?今天,就让我带你...
现在安卓随便换系统,探索个性化... 你知道吗?现在安卓手机换系统简直就像换衣服一样简单!没错,就是那种随时随地、随心所欲的感觉。今天,就...
安卓系统安装按钮灰色,探究原因... 最近发现了一个让人头疼的小问题,那就是安卓手机的安装按钮突然变成了灰色,这可真是让人摸不着头脑。你知...
安卓7.1.1操作系统,系统特... 你知道吗?最近我在手机上发现了一个超级酷的新玩意儿——安卓7.1.1操作系统!这可不是什么小打小闹的...
安卓os系统怎么设置,并使用`... 你有没有发现,你的安卓手机有时候就像一个不听话的小孩子,有时候设置起来真是让人头疼呢?别急,今天就来...
安卓降低系统版本5.1,探索安... 你知道吗?最近安卓系统又来了一次大动作,竟然把系统版本给降到了5.1!这可真是让人有点摸不着头脑,不...
解放安卓系统被保护,解放安卓系... 你有没有想过,你的安卓手机其实可以更加自由地呼吸呢?是的,你没听错,我说的就是解放安卓系统被保护的束...