当前位置: 首页 > 科技观察

关于Go程序错误处理的一些建议

时间:2023-03-18 15:23:24 科技观察

关于Go程序错误处理的几点建议本文转载请联系网管谢bi公众号。Go的错误处理是人们每天抱怨很多的地方。我在工作中也观察到一些现象。比较严重的是每一级逻辑代码中的错误处理有些重复。例如,如果有人写代码,他会在每一层判断错误并记录日志。从代码层面看,似乎很严谨,但是如果看日志,会发现一堆重复的信息,对排错的时候会造成干扰。今天,我就为大家总结出Go代码错误处理相关的三个最佳实践。这些最佳实践也被一些前辈分享在网上。在我自己练习之后,我在这里用我自己的语言描述它们。我希望他们能对大家有所帮助。理解errorGo程序是通过错误类型的值来表达错误的。error类型是一个内置的接口类型,它只指定一个返回字符串值的Error方法。typeerrorinterface{Error()string}Go语言函数经常返回一个错误值,调用者通过测试错误值是否为nil来进行错误处理。i,err:=strconv.Atoi("42")iferr!=nil{fmt.Printf("couldn'tconvertnumber:%v\n",err)return}fmt.Println("Convertedinteger:",i)错误是无表示成功;非零错误表示失败。自定义错误记得实现错误接口我们经常会定义符合我们需要的错误类型,但是记得让这些类型实现错误接口,这样就不需要在调用者的程序中引入额外的类型。例如,我们在下面定义类型myError。如果错误接口没有实现,调用者的代码就会被myError类型侵入。比如下面的run函数,在定义返回值类型的时候,可以直接定义为error。packagemyerrormport("fmt""time")typemyErrorstruct{CodeintWhentime.TimeWhatstring}func(e*myError)Error()string{returnfmt.Sprintf("at%v,%s,code%d",e.When,e.What,e.Code)}funcrun()error{return&MyError{1002,time.Now(),"itdidn'twork",}}funcTryIt(){iferr:=run();err!=nil{fmt.Println(err)}}如果myError没有实现错误接口,这里的返回值类型必须定义为myError类型。可以想象,调用者的程序必须通过myError.Code==xxx来判断具体是哪种错误(当然,如果要这样做,首先要将myError改成导出的MyError)。调用者在判断自定义错误是哪种错误时应该怎么办呢?MyError没有暴露在包外。答案是将检查错误行为的方法暴露在包外。myerror.IsXXXError(err)...或者通过比较error本身是否等于包暴露的常量error来判断,比如io.EOF,在操作a时常用于判断文件是否结束文件。同样,还有各种开源包公开的错误常量,例如gorm.ErrRecordNotFound。iferr!=io.EOF{returnerr}错误处理常见错误首先看一个简单的程序,看看能不能发现一些细微的问题funcWriteAll(wio.Writer,buf[]byte)error{_,err:=w.Write(buf)iferr!=nil{log.Println("unabletowrite:",err)//annotatederrorgoestologfilereturnerr//unannotatederrorreturnedtocaller}returnnil}funcWriteConfig(wio.Writer,conf*Config)error{buf,err:=json.Marshal(conf)iferr!=nil{log.Printf("couldnotmarshalconfig:%v",err)returnerr}iferr:=WriteAll(w,buf);err!=nil{log.Println("couldnotwriteconfig:%v",err)returnerr}returnnil}funcmain(){err:=WriteConfig(f,&conf)fmt.Println(err)//io.EOF}错误处理两个常见问题上面程序的错误处理暴露了两个问题:底层发生错误后函数WriteAll,除了向上层返回错误外,还会在日志中记录错误。上层调用者做同样的事情,记录日志然后将错误返回给程序的顶层。于是我在日志文件unablettowrite:io.EOFcouldnotwriteconfig:io.EOF...中得到了一堆重复的内容2.在程序最上面,虽然得到了原来的错误,但是没有相关的内容,换句话说,WriteAll,WriteConfigwerenotrecorded日志中的信息打包成错误返回给上层。这两个问题的解决方案可以是在底层函数WriteAll和WriteConfig发生的错误中加入上下文信息,然后将错误返回给上层,最终由上层程序来处理这些错误。包装错误的一种简单方法是使用fmt.Errorf函数,它将信息添加到错误中。funcWriteConfig(wio.Writer,conf*Config)error{buf,err:=json.Marshal(conf)iferr!=nil{returnfmt.Errorf("couldnotmarshalconfig:%v",err)}iferr:=WriteAll(w,buf);err!=nil{returnfmt.Errorf("couldnotwriteconfig:%v",err)}returnnil}funcWriteAll(wio.Writer,buf[]byte)error{_,err:=w.Write(buf)iferr!=nil{returnfmt.Errorf("writefailed:%v",err)}returnnil}为错误添加上下文信息fmt.Errorf只是为错误添加简单的注解信息,如果想在添加信息Stack的同时添加错误的调用,您可以使用包github.com/pkg/errors提供的错误打包功能。//只添加新信息funcWithMessage(errerror,messagestring)error//只附加调用堆栈信息funcWithStack(errerror)error//同时添加堆栈和信息funcWrap(errerror,messagestring)error有一个包装方法,有一个对应的解包方法method,Cause方法会返回打包错误对应的最原始的错误——也就是递归解包。funcCause(errerror)error下面是使用github.com/pkg/errors重写的错误处理函数funcReadFile(pathstring)([]byte,error){f,err:=os.Open(path)iferr!=nil{returnnil),errors.Wrap(err,"openfailed")}deferf.Close()buf,err:=ioutil.ReadAll(f)iferr!=nil{returnnil,errors.Wrap(err,"readfailed")}returnbuf,nil}funcReadConfig()([]byte,error){home:=os.Getenv("HOME")config,err:=ReadFile(filepath.Join(home,".settings.xml"))returnconfig,errors.WithMessage(err"couldnotreadconfig")}funcmain(){_,err:=ReadConfig()iferr!=nil{fmt.Printf("originalerror:%T%v\n",errors.Cause(err),errors.Cause(err))fmt.Printf("stacktrace:\n%+v\n",err)os.Exit(1)}}上面用来格式化字符串的%+v是在%v的基础上扩展了数值,即扩展复合类型值,如结构体的字段值等细节。这样就可以在错误中加入调用堆栈信息,同时保留对原错误的引用。通过Cause,可以恢复错误的原始原因。总结一下,错误处理的原则是:错误只在最外层逻辑处理一次,底层只返回错误。除了返回错误之外,底层还需要对原始错误进行封装,并添加有助于排错的错误信息、调用栈等上下文信息。