Go进阶-Error

Error vs. Exception

Error

Go Error就是普通的一个接口,通过该接口得到一个普通的值
返回string的Error方法,可以知道错误的一些详细信息

type error interface {
    Error() string
}

我们经常使用errors.New()来返回一个error对象

type errorString struct{
    s string
}
func (e *errorString) Error() string{
    return e.s
}

基础库中大量自定义的error
建议在error.New时前面用 ‘包名: 错误具体信息’,这样可以知道错误是从哪个包抛出来的,会清晰一点。

var {
    ErrInvalidUnreadByte = error.New("bufio: invalid use of UnreadByte")
    ErrInvalidUnreadRune = error.New("bufio: invalid use of UnreadRune")
    ErrBufferFull = error.New("bufio: buffer full")
    ErrNegativeCount = error.New("bufio: negative count")
}

errors.New()返回的是内部errorString对象的指针,这里之所以使用指针,是为了避免两个人使用了一样的字符串返回错误。

//New returns an error that formats as the given text.
func New(text string) error{
    return &errorString{text}
}
package main

import (
	"errors"
	"fmt"
)

//Create a named type for our new error type
type errorString string

//Implement the error interface.
func (e errorString) Error() string {
	return string(e)     //类型强制转换
}

//New creates interface values of type error.
func New(text string) error {
	return errorString(text)   //传参,传了具体的错误内容
}

var ErrNamedType = New("EOF")   //自己构建的EOF
var ErrStructType = errors.New("EOF")    //标准库的EOF

func main() {
	if ErrNamedType == New("EOF") {
		fmt.Println("Named Type Error")
	}
	if ErrStructType == errors.New("EOF") {
		fmt.Println("Struct Type Error")
	}
}

Output: Named Type Error
说明两种New得到的是一致的,==判定是相等的。但是对于标准库生成的,两个生成的指针指向它们的内容肯定是不一样的,内存地址不同。不能光去用字符串判定,所以前面用了指针。

package main

import "fmt"

type errorString struct {
	s string
}

func (e errorString) Error() string {
	return e.s
}
func NewError(text string) error {
	return errorString{text}       //跟标准库一模一样,唯一不一样的就是把指针去掉了
}

var ErrType = NewError("EOF")

func main() {
	if ErrType == NewError("EOF") {
		fmt.Println("Error:", ErrType)
	}
}

Output: Error: EOF
这样判定出来会相等,所以用指针,取地址很重要。

Error vs Exception

各个语言的演进历史:

  • C
    单返回值,一般用指针作为入参,返回值为int,表示成功或失败。
  • C++
    引入了exception, 但是无法知道被调用方会抛出什么异常。
  • Java
    引入了checked exception, 方法的所有者必须申明,调用者必须处理。在启动时抛出大量的异常是司空见惯的事,它们在调用堆栈中都被尽职地记录下来,这使得Java异常不再是异常,而是变得司空见惯了。异常类型从良性到灾难性都有,严重性由函数的调用者来区分。

Go的处理异常逻辑是不引入exception,并支持多参数返回,所以你很容易的在函数签名中带上实现了error interface的对象,交由调用者来判定。
如果一个函数返回了(value,error), 你不能对这个value做任何假设,必须先判定error。唯一可以忽略error的情况是,你对value也不关心。

Go中有panic的机制,如果你认为和其他语言的exception一样,那你就错了。当我们抛出异常的时候,相当于你把exception扔给了调用者来处理。
比如,你在C++中,把string转为int,如果转换失败,会抛出异常。或者在Java中转换String为Date失败时,会抛出异常。
Go panic意味着fatal error(就是挂了),代码不能继续运行,所以不能假设让调用者来解决panic。
通过使用多个返回值和一个简单的约定,Go解决了让程序员知道什么时候出了问题,并为真正的异常情况保留了panic。

throw new SomeoneElsesProblem();
panic("inconceivable")

package main

import "fmt"

func handle() (int, error) {     //通常是第一个返回需要的结果,第二个返回error
	return 1, nil
}
func main() {
	i, err := handle()
	if err != nil {
		return
	}
	fmt.Println(i)
}

一个判断正负0的代码逐步修改的过程,首先只能判断正负

package main

import "fmt"

//Positive returns true if the number is positive, false if it is negative
func Positive(n int) bool {
	return n > -1
}
func Check(n int) {
	if Positive(n) {
		fmt.Println(n, "is positive")
	} else {
		fmt.Println(n, "is negative")
	}
}
func main() {
	Check(1)
	Check(0)
	Check(-1)
}

通过返回两个bool类型返回值,达到可以判断正负0的要求

package main

import "fmt"

//Positive returns true if the number is positive, false if it is negative
//The second return value indicates if the result is valid, which in the case of n==0 is not valid.
func Positive(n int) (bool, bool) {
	if n == 0 {
		return false, false
	}
	return n > -1, true
}
func Check(n int) {
	pos, ok := Positive(n)
	if !ok {
		fmt.Println(n, "is neither")
		return
	}
	if pos {
		fmt.Println(n, "is positive")
	} else {
		fmt.Println(n, "is negative")
	}
}
func main() {
	Check(1)
	Check(0)
	Check(-1)
}

对于判断0,改为返回error类型,判断其是否为空来判断是否为0

package main

import (
	"errors"
	"fmt"
)

//Positive returns true if the number is positive, false if it is negative
func Positive(n int) (bool, error) {
	if n == 0 {
		return false, errors.New("undefined")
	}
	return n > -1, nil
}
func Check(n int) {
	pos, err := Positive(n)
	if err != nil {
		fmt.Println(n, err)
		return
	}
	if pos {
		fmt.Println(n, "is positive")
	} else {
		fmt.Println(n, "is negative")
	}
}
func main() {
	Check(1)
	Check(0)
	Check(-1)
}

把返回值改为bool指针,因为指针可以设为空值
对于这一种,是返回nil好,还是返回空数组好呢?可以思考

package main

import (
	"fmt"
)

//If the result not nil, the result is true if the number is positive, false if it is negative.
func Positive(n int) *bool {
	if n == 0 {
		return nil
	}
	r := n > -1
	return &r
}
func Check(n int) {
	pos := Positive(n)
	if pos == nil {
		fmt.Println(n, "is neither")
		return
	}
	if *pos {
		fmt.Println(n, "is positive")
	} else {
		fmt.Println(n, "is negative")
	}
}
func main() {
	Check(1)
	Check(0)
	Check(-1)
}

利用panic来处理为0的情况
panic 会导致栈被展开直到 defer 修饰的 recover () 被调用或者程序中止。
recover 只能在 defer 修饰的函数中使用:用于取得 panic调用中传递过来的错误值,如果是正常执行,调用 recover 会返回 nil,且没有其它效果。
这样做是不好的,panic这样用不好,panic就应该用来处理fatal error。

package main

import (
	"fmt"
)

//If the result not nil, the result is true if the number is positive, false if it is negative.
func Positive(n int) bool {
	if n == 0 {
		panic("undefined")
	}
	return n > -1
}
func Check(n int) {
	defer func() {
		if recover() != nil {
			fmt.Println("is neither")
		}
	}()
	if Positive(n) {
		fmt.Println(n, "is positive")
	} else {
		fmt.Println(n, "is negative")
	}
}
func main() {
	Check(1)
	Check(0)
	Check(-1)
}

对于真正意外的情况,那些表示不可恢复的程序错误,例如索引越界、不可恢复的环境问题、栈溢出,我们才使用panic。对于其他的错误情况,我们应该是期望使用error来进行判定。
You only need to check the error value if you care about the result. --Dave

Guy AddNewGuy(string name){
    Guy guy=new Guy(name);
    AddToLeague(guy);
    guy.Team=ChooseRandomTeam();
    return guy;
}

对于以上代码,如果使用Exception,在到处都有可能出错。所以Exception就很可能遇到很复杂的情况。

  • 简单
  • 考虑失败,而不是成功
  • 没有隐藏的控制流
  • 完全交给你来控制Error
  • Error are values

Error Type

Sentinel Error

预定义的特定错误,我们叫sentinel error,这个名字来源于计算机编程中使用一个特定值来表示不可能进行进一步处理的做法。所以对于Go,我们使用特定的值来表示错误。
if err==ErrSomething{...}
类似的io.EOF,更底层的syscall.ENOENT

type Error string
func (e Error) Error() string { return string(e)}

使用sentinel值是最不灵活的错误处理策略,因为调用方必须使用“ == ”将结果与预先声明的值进行比较。当你想要提供更多的上下文时就会出现问题,因为返回一个不同的错误将会破坏相等性检查。
甚至一些有意义的fmt.Errorf携带一些上下文时,也会破坏调用者的“ == ”,调用者将被迫查看error.Error()方法的输出,以检查它是否与特定的字符串匹配。

  • 不依赖检查error.Error的输出
    不应该依赖检测error.Error的输出,Error方法存在于error接口,它主要是给程序员使用的(调试),但不是给程序使用的(编写测试可能会依赖这个返回)。这个输出的字符串用于记录日志、输出到stdout等地方。
  • Sentinel errors成为你API公共部分
    • 如果你的公共函数或方法返回一个特定值的错误,那么该值必须是公共的,当然要有文档记录,这会增加API的表面积。
    • 如果API定义了一个返回特定错误的interface,则该接口的所有实现都将被限制为仅返回该错误,即使它们可以提供更具描述性的错误。
      比如io.Reader:像io.Copy这类函数需要reader的实现者(比如返回io.EOF)来告诉调用者没有更多数据了,但这又不是错误。
  • Sentinel errors在两个包之间创建了依赖。
    • sentinel errors最糟糕的问题是它们在两个包之间创建了源代码依赖关系。例如,检查错误是否等于io.EOF,你的代码必须导入IO包。这个特定的例子听起来并不那么糟糕,因为它非常常见。但想象一下这种场景:当项目中的许多包导出错误值时会存在耦合的情况,而项目中的其他包必须导入这些错误值才能检查特定的错误条件(in the form of an import loop)
  • 结论:尽可能避免sentinel errors
    在标准库中有一些使用它们的情况,但这不是一个你应该模仿的模式。

Error Type

Error Type实现了error接口的自定义类型。例如MyError类型记录了文件和行号以展示发生了什么。

type MyError struct{
    Msg string
    File string
    Line int
}
func (e *MyError) Error() string{
    return fmt.Sprintf("%s:%d: %s",e.File, e.Line, e.Msg)
}
func test() error{
    return &MyError{"Something happened","server.go",42}
}

因为MyError是一个type,调用者可以使用断言转换成这个类型,来获取更多的上下文信息。

func main(){
    err:=test()
    switch err := err.(type){
    case nil:
        // call succeeded, nothing to do
    case *MyError:
        fmt.Println("error occurred on line:", err.Line)
    default:
        // unknown error
    }
}

与错误值相比,错误类型的一大改进是它们能够包装底层错误以提供更多上下文。
一个不错的例子就是os.PathError,它提供了底层执行了什么操作、哪个路径出了什么问题等信息。
type PathError

type PathError struct{
    Op string
    Path string
    Err error
}

PathError records an error and the operation and file path that caused it.

func (*PathError) Error
func (e *PathError) Error() string

调用者要使用类型断言和类型switch,就要让自定义的error变为public。这种模型会导致与调用者产生强耦合,从而导致API变得脆弱。
结论是尽量避免使用error types,虽然错误类型比sentinel errors更好,因为它们可以捕获关于出错的更多上下文,但是error types共享error values许多相同的问题。

  • 因此,我的建议是避免错误类型,或者至少避免将它们作为公共API的一部分

Opaque errors

不透明的错误处理

在我看来,这是最灵活的错误处理策略,因为它要求代码和调用者之间的耦合最少。
只需要判断err!=nil
我将这种风格称为不透明错误处理,因为虽然你知道发生了错误,但你没有能力看到错误的内部。作为调用者,关于操作的结果,你所知道的就是它起作用了,或者没有起作用(成功还是失败)。
这就是不透明错误处理的全部功能——只需返回错误而不假设其内容。

import "github.com/quux/bar"

func fn() error {
	x, err := bar, Foo()
	if err != nil {
		return err
	}
	//use x
}

Assert errors for behaviour, not type
在少数情况下,这种二分错误处理方法是不够的。例如,与进程外的世界进行交互(如网络活动),需要调用方调查错误的性质,以确定重试该操作是否合理。
在这种情况下,我们可以断言错误实现了特定的行为,而不是断言错误是特定的类型或值。

以下代码不好的地方在于net.Error的interface是对外暴露的

package net

type Error interface{
    error
    Timeout() bool   //Is the error a timeout?
    Temporary() bool  //Is the error temporary?
}
if nerr,ok:=err.(net.Error);ok&&nerr.Temporary() {
    time.Sleep(1e9)
    continue
}
if err !=nil{
    log.Fatal(err)
}

以下代码展示的方法没有对外暴露任何自定义的错误类型,而是通过一个内置的interface在内部断言,然后返回方法

type temporary interface{    //小写,没有暴露的接口
    Temporary() bool
}

//IsTemporary returns true is err is temporary.
func IsTemporary(err error) bool{
    te,ok:=err.(temporary)      //对错误做一个断言
    return ok && te.Temporary()
}

这里的关键是,这个逻辑可以在不导入定义错误的包或者实际上不了解err的底层类型的情况下实现——我们只对它的行为感兴趣。

Handling Error

Indented flow is for errors

无错误的正常流程代码,将成为一条直线,而不是缩进的代码

//建议的做法,先处理有错误,后处理没错误
f,err:=os.Open(path)
if err!=nil{
    //handle error
}
//do stuff

//不建议的做法,因为会导致正确的做法全部被缩进
f,err:=os.Open(path)
if err==nil{
    //do stuff
}
//handle error

Eliminate error handling by eliminating errors

消除错误

下面的代码有什么问题?
代码根据Request请求参数,来验证user是否合法
问题在于多写了很多代码

func AuthenticateRequest(r *Request) error{
    err:=authenticate(r.User)
    if err!=nil{
        return err
    }
    return nil
}

上述代码可以改进为下面这种,直接全部return回去就好了

func AuthenticateRequest(r *Request) error{
    return authenticate(r.User)
}

统计io.Reader读取内容的行数

func CountLines(r io.Reader) (int, error) {
	var (
		br    = bufio.NewReader(r)
		lines int
		err   error
	)
	for {
		_, err = br.ReadString('\n')
		lines++  //如果有io.EOF的话,跳过就会少记录一行,因此先line++再判断err!=nil
		if err != nil {
			break
		}
	}
	if err!=io.EOF{    //如果不是EOF说明读的中间报错了
		return 0,err
	}
	return lines,nil
}

但是上面的代码也写的比较多,以下是改进版本:

func CountLines(r io.Reader) (int, error) {
	sc:=bufio.NewScanner(r)
	lines:=0
	for sc.Scan(){      //Scan方法会返回true/false,还有下一行返回true,没有返回false
		lines++
	}
	return lines,sc.Err()    //如果中间有错误就跳出了上面的循环,此时会有error,获取scanner内部错误
}

WriteResponse
下面代码处理了三次if err!=nil,多写了很多代码

type Header struct{
	Key, Value string
}
type Status struct{
	Code int
	Reason string
}
func WriteResponse(w io.Writer,st Status, headers []Header, body io.Reader) error{
	//首先把Code和message拼进去
	_,err:=fmt.Fprintf(w,"HTTP/1.1 %d %s\r\n",st.Code,st.Reason)
	if err!=nil{
		return err
	}
	for _,h:=range headers{
		_,err:=fmt.Fprintf(w,"%s: %s\r\n",h.Key,h.Value)
		if err!=nil{
			return err
		}
	}
	if _,err:=fmt.Fprintf(w,"\r\n"); err!=nil{
		return err
	}
	_,err=io.Copy(w,body)
	return err
}

改进

type errWriter struct{
	io.Writer
	err error     //类似于Scanner和Rows,暂存error
}
func (e *errWriter) Write(buf []byte) (int,error){
	if e.err!=nil{      //没有处理任何内容
		return 0,e.err
	}
	var n int
	n,e.err=e.Writer.Write(buf)
	return n,e.err
}

连接上面的代码,改进后的WriteResponse
把很多细节藏到了errWriter当中

func WriteResponse(w io.Writer, st Status, headers []Header, body io.Reader) error {
	ew := &errWriter{Writer: w}
	fmt.Fprintf(ew,"HTTP/1.1 %d %s\r\n",st.Code,st.Reason)
	for _,h:=range headers{
		fmt.Fprintf(ew,"%s: %s\r\n",h.Key,h.Value)
	}
	fmt.Fprint(ew,"\r\n")
	io.Copy(ew,body)
	return ew.err
}

Wrap Errors

还记得之前我们auth的代码吧,如果authenticate返回错误,则AuthenticateRequest会将错误返回给调用方,调用者可能也会这样做,依此类推。在程序的顶部,程序的主体将把错误打印到屏幕或日志文件中,打印出来的只是:没有这样的文件或目录。
打不出任何东西,因为错误是一层层往上抛的,假设说底层抛了一个预定义错误sentinel error,当你往上抛的时候,别人可能会用==做一些判定,所以你不敢用fmt.Error去做一些包装,因为包装一些更详细的上下文的时候,上面的==判定肯定会报错,只能一层层网上透穿,透到最顶层打印,就是没有任何详细错误信息。

func AuthenticateRequest(r *Request) error{
    return authenticate(r.User)
}

所以鼓励大家在任何地方打印错误的详细信息。
没有生成错误的file:line信息,没有产生错误代码的stack信息。这段代码的作者将被迫进行长时间的代码分割,以发现是哪个代码路径触发了文件未找到错误。

func AuthenticateRequest(r *Request) error{
	err:=authenticate(r.User)
	if err!=nil{
		return fmt.Errorf("authenticate failed: %v",err)
	}
	return nil
}

但是正如我们前面看到的,这种模式与sentinel errors或type assertions的使用不兼容,因为它将错误值转换为字符串,将其与另一个字符串合并,然后将其转换回fmt.Errorf从而破坏了原始错误,导致等值判定失败。

You should only handle errors once. Handling an error means inspecting the error value, and making a single decision.
你应该只处理错误一次。

//WriteAll writes the contents of buf to the supplied writer.
func WriteAll(w io.Writer, buf []byte){
    w.Write(buf)
}

我们经常发现类似的代码,在错误处理中,带了两个任务:记录日志并且再次返回错误。
以下代码处理了错误两次,第一个打印了日志,第二个把错误往上抛,但其实应该只做一件事。

func WriteAll(w io.Writer, buf []byte) error {
	_, err := w.Write(buf)
	if err != nil {
		log.Println("unable to write:", err) //annotated(注释的) error goes to log file
		return err                           //unannotated error returned to caller
	}
	return nil
}

在这个例子中,如果在w.Write过程中发生了一个错误,那么一行代码将被写入日志文件中,记录错误发生的文件和行,并且错误也会返回给调用者,调用者可能会记录并返回它,一直返回到程序的顶部。

func WriterConfig(w io.Writer,conf *Config) error{
	buf,err:=json.Marshal(conf)     //把配置文件序列化
	if err!=nil{
		log.Printf("could not marshal config: %v",err)
		return err
	}
	if err:=WriteAll(w,buf);err!=nil{
		log.Println("could not write config:%v",err)
		return err
	}
	return nil
}
func main() {
    err:=WriteConfig(f, &conf)
    fmt.Println(err) //io.EOF
}

unable to write:io.EOF
could not write config:io.EOF

Go中错误处理契约规定:在出现错误的情况下,不能对其他返回值的内容作出任何假设。由于JSON序列化失败,buf的内容是未知的,可能它不包含任何内容,但更糟糕的是,它可能包含一个半写的JSON片段。
由于程序员在检查并记录错误后忘记return,损坏的缓冲区将被传递给WriteAll,这可能会成功,因此配置文件将被错误地写入。但是,该函数返回的结果是正确的。
对于错误能处理就应该处理了,不能处理就网上抛return nil

func WriterConfig(w io.Writer,conf *Config) error{
	buf,err:=json.Marshal(conf)
	if err!=nil{
		log.Printf("could not marshal config: %v",err)
		//oops, forgot to return
	}
	if err:=WriteAll(w,buf);err!=nil{
		log.Println("could not write config:%v",err)
		return err
	}
	return nil
}

日志记录与错误无关且对调试没有帮助的信息应被视为噪音,应予以质疑。
记录的原因是因为某些东西失败了,而且日志包含了答案。

  • The error has been logged. 错误要被日志记录
  • The application is back to 100% integrity. 应用程序处理错误,保证100%完整性
  • The current error is not reported any longer. 之后不再报告当前错误

外部包github.com/pkg/errors
终端输入命令:go get github.com/pkg/errors
Wrap包帮助保留了原始错误,同时还能捎带一些扩展信息

func ReadFile(path string)([]byte, error){
	f,err:=os.Open(path)
	if err!=nil{
		return nil,errors.Wrap(err,"open failed")
	}
	defer f.Close()
}
func main() {
	_, err := ReadConfig()
	if err != nil {
		fmt.Printf("original error: %T %v\n", errors.Cause(err), errors.Cause(err))
		//errors.Cause获取根因,获取原始错误
		fmt.Printf("stack trace:\n%+v\n", err)  //堆栈追踪
		os.Exit(1)
	}
}

WithMessage和之前的Wrap基本没有区别,只是Wrap会保存堆栈信息,而WithMessage不会,因为ReadFile已经保存过了。因为底层错误是上面代码抛出的,这个地方只是附加了一些信息,所以它叫WithMessage。

func ReadConfig() ([]byte, error) {
	home := os.Getenv("HOME")
	config, err := ReadFile(filepath.Join(home, ".settings.xml"))
	return config, errors.WithMessage(err, "could not read config")
}
func main() {
	_, err := ReadConfig()
	if err != nil {
		fmt.Println(err)
		os.Exit(1)
	}
}

原始错误是路径错误,能拿到原始类型

对于处理错误两次的优化如下,也就是出现错误就Wrap起来,不记日志,就一层层往上抛,抛到最上面比方说可能是net/http,可以在第一个中间件里把日志加上,那就可以获取一个完整的请求链路的参数,请求什么接口,成功还是失败,堆栈是什么。如果报错的话堆栈信息会保存出来,排查错误的话很方便。errors.Cause也可以进行sentinel error的等值判定。

func Write(w io.Writer, buf []byte) error{
	_,err:=w.Write(buf)
	return errors.Wrap(err,"write failed")
}

通过使用pkg/errors包,你可以向错误值添加上下文,这种方式既可以由人也可以由机器检查。

func Write(w io.Writer, buf []byte) error {
	_, err := w.Write(buf)
	if err!=nil{
		//annotated error goes to log file
		log.Println("unable to write:",err)
		//unannotated error returned to caller
		return err
	}
	return nil
}

Wrap errors的使用技巧
在你的应用代码中,使用errors.New或者errors.Errorf返回错误。
以上两个方法都会携带原始堆栈信息,就是哪个地方报的错,原始的堆栈信息都会记录下来。

func parseArgs(args []string) error{
    if len(args)<3{
        return errors.Errorf("not enough arguments,expected at least...",err)
    }
    //...
}

如果调用其他包内的函数,通常直接返回错误。
满足了只处理错误一次,如果调用其他包内错误函数直接往上抛。

if err !=nil{
    return err
}
  • 如果和其他库(第三方库或者标准库)进行协作,考虑使用errors.Wrap或者errors.Wrapf保存堆栈信息。
    这种做法同样适用于和标准库写作的时候。
f,err:=os.Open(path)
if err!=nil{
    return errors.Wrapf(err,"failed to open %q",path)
}
  • 直接返回错误,而不是在每个错误产生的地方都打出日志。
  • 在程序的顶部或者是工作的goroutine顶部(请求入口),使用%+v详细记录堆栈详情。
func main() {
	err:=app.Run()
	if err!=nil{
		fmt.Printf("FATAL: %+v\n",err)
		os.Exit(1)
	}
}
  • 使用errors.Cause获取root error,再和sentinel error判定。

总结:

  • Packages that are reusable across many projects only return root error values.
    如果一个包被很多人使用,那么不应该使用任何的wrap包装错误,应该返回它的根因,就是原始错误是什么就是什么。
    选择wrap error是只有applications可以选择应用的策略,具有最高可重用性的包只能返回根错误值。此机制与Go标准库中使用的相同(kit库的sql.ErrNoRows)。
    肯定不会返回一个包装的信息给你,因为你这个情况下不知道什么时候需要wrap。
  • 使用errors.Cause获取root error,再和sentinel error判定。
  • If the error is not going to be handled, wrap and return up the call stack.
    这是关于函数/方法调用返回每个错误的基本问题。如果函数/方法不打算处理错误,那么用足够的上下文wrap errors并将其返回到调用堆栈中。
    错误能处理就处理,如果处理不了就带上足够多的信息往上抛。
    例如,额外的上下文可以是使用的输入参数或失败的查询语句。确定你记录的上下文是足够多还是太多的一个好方法是检查日志并验证它们在开发期间是否为你工作。
  • 使用errors.Cause获取root error,再进行和sentinel error判定。
  • Once an error is handled, it is not allowed to be passed up the call stack any longer.
    一旦确定函数/方法将处理错误,错误就不再是错误。如果函数/方法仍然需要发出返回,则它不能返回错误值。它应该只返回零(比如降级处理中,你返回了降级数据,然后需要return nill)。

Go 1.13 errors

Errors before Go 1.13

最简单的错误检查

if err!=nil{
    //something went wrong
}

有时我们需要对sentinel error进行检查

var ErrNotFound=errors.New("not found")

if err==ErrNotFound{
    //something wasn't found
}

实现了error interface的自定义error struct,使用断言来获取更丰富的上下文。

type NotFoundError struct{
    Name string
}
func (e *NotFoundError) Error() string { return e.Name+": not found" }
if e,ok:=err.(*NotFoundError);ok{
    //e.Name wasn't found
}

函数在调用栈中添加信息向上传递错误,例如简要描述错误发生时的情况。

if err!=nil{
    return fmt.Errorf("decompress %v: %v",name,err)
}

使用创建新的错误fmt.Errorf会丢弃原始错误中除文本外的所有内容。正如我们在上面的QueryError中看到的那样,我们有时可能需要定义一个包含底层错误的新错误类型,并将其保存以供代码检查。这里是QueryError:

type QueryError struct{
    Query string
    Err error
}

程序可以查看QueryError值以便根据底层错误做出决策。只需要拿到error进行一个断言

if e,ok:=err.(*QueryError);ok && e.Err==ErrPermission{
    //query failed because of a permission problem
}

Unwrap

Go 1.13为errors和fmt标准库包引入了新特性,以简化处理包含其他错误的错误。其中最重要的是:包含另一个错误的error可以实现返回底层错误的Unwrap方法。如果e1.Unwrap()返回e2,那么我们说e1包装e2,你可以展开e1以获得e2。

按照此约定,我们可以为上面的QueryError类型指定一个Unwrap方法,该方法返回其包含的错误:
func (e *QueryError) Unwrap() error{ return e.Err }
Go 1.13 errors包包含两个用于检查错误的新函数:Is和As

//Similar to:
//if err==ErrNotFound{...}
if errors.Is(err, ErrNotFound){
    //something wasn't found
}
//Similar to:
//  if e,ok:=err.(*QueryError); ok {...}
var e *QueryError 
//Note: *QueryError is the type of the error.
if errors.As(err, &e){
    //err is a *QueryError, and e  is set to the error's value
}
if errors.Is(err,ErrPermission){
    //err,or some error that it wraps, is a permission problem
}

Wrapping errors with %w

如前所述,使用fmt.Errorf向错误添加附加信息。

if err!=nil{
    return fmt.Errorf("decompress %v: %v",name,err)
}

在Go 1.13中fmt.Errorf支持新的%w谓词。%w相当于内部会把对象包装起来。

if err!=nil{
    //Return an error which unwraps to err.
    return fmt.Errorf("decompress %v: %w",name,err)
}

用%w包装错误可用于errors.Is以及errors.As:

err:=fmt.Errorf("access denied: %w", ErrPermission)
...
if errors.Is(err, ErrPermission) ...

Wrapping errors with %w 内部实现

type wrapError struct{
	msg string
	err error
}
func (e *wrapError) Error() string{
	return e.msg
}
func (e *wrapError) Unwrap() error{
	return e.err
}

Customizing error tests with Is and As methods

Go1.13的Is:判断是否实现了Is方法,假设没有实现,就会不断调用Unwrap去把根因的错误再去拿到。如果拿到根因错误后,最终我们看到第一行代码,它会判断err和target对象,target对象就是目标对象是否相等,如果相等则返回true。

func Is(err, target error) bool {
	if target == nil {
		return err == target
	}

	isComparable := reflectlite.TypeOf(target).Comparable()
	for {
		if isComparable && err == target {
			return true
		}
		if x, ok := err.(interface{ Is(error) bool }); ok && x.Is(target) {
			return true
		}
		// TODO: consider supporting target.Is(err). This would allow
		// user-definable predicates, but also may allow for coping with sloppy
		// APIs, thereby making it easier to get away with them.
		if err = Unwrap(err); err == nil {
			return false
		}
	}
}
type Error struct {
	Path string
	User string
}
func (e *Error) Is(target error) bool{   //扩展实现Is方法
	t,ok:=target.(*Error)
	if !ok{
		return false
	}
	return (e.Path==t.Path || t.Path=="") && (e.User==t.User || t.User=="")
}
if errors.Is(err, &Error{User: "someuser"}){
	// err's User field is "someuser".
}

以前的做法如下。以前的做法只能依赖ErrPermission的定义,无法追加更多的上下文信息,最终的调试成本会变得非常高。

var ErrPermission=errors.New("permission denied")
//DoSomething returns an error wrapping ErrPermission if the user
//does not have permission to do something.
func DoSomething() error{
	if !userHasPermission(){
		//If we return ErrPermission directly, callers might come
		//to depend on the exact error value, writing code like this:
		//    if err:=pkg.DoSomething(); err==pkg.ErrPermission{...}
		//This will cause problems if we want to add additional
		//context to the error in the future. To avoid this, we 
		// return an error wrapping the sentinel so that users must 
		//always unwrap it:
		//    if err:=pkg.DoSomething();errors.Is(err, pkg.ErrPermission) {...}
		return fmt.Errorf("%w", ErrPermission)
	}
	// ...
}

目前来看还是得继续使用pkg/errors,它包含了堆栈信息。

import (
	"errors"
	"fmt"

	xerrors "github.com/pkg/errors"
)

var errMy = errors.New("my")

func main() {
	err := test2()
	fmt.Printf("main: %+v\n", err)
}
func test0() error {
	return xerrors.Wrapf(errMy, "test0 failed")
}
func test1() error {
	return test0()
}
func test2() error {
	return test1()
}
  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值