Go 使用errors库构造错误堆栈

485 阅读4分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第15天,点击查看活动详情

转GO语言有了差不多8个月的时间,之前是做java相关的工作,因为java语言是一门非常受欢迎的语言,他强大的生态和语言本身的魅力,让我们在编写代码的时候非常的舒适,比如java的异常机制,java的动态代理,接口,多态,继承等等特性使得我们在编码的时候非常的舒服,写出的代码也非常的优雅,规范,但是自从转了Go之后,发现Go语言很多东西都设计的非常的简单,如,Go的容器,List,Map等,最让我感到编程不舒服的就是GO的错误处理,经过一段时间的研究,发现了一个非常好用的Go错误处理的库,今天分享给大家~希望对大家的工作能有所帮助~~~

糟糕的代码

我们在编写业务代码的时候经常会有类似这种代码

if err != nil {
    log.Error("err info: %v",err)
    return err
}

如果我们编写了一个模板方法,那么一半的代码都是在进行错误处理,如:

res1, err := Method1()
if err != nil {
    log.Error("method1 err, err info: %v",err)
    return err
}
res2, err := Method2()
if err != nil {
    log.Error("method2 err, err info: %v",err)
    return err
}
res3, err := Method3()
if err != nil {
    log.Error("method3 err, err info: %v",err)
    return err
}

这种编码方式,使得我们的代码变得非常的臃肿,为什么需要这么写呢?因为Go的error只是一个接口,在打印error的时候并不会打印具体的错误堆栈信息。为了便于排查问题,不得已,只能通过导出打日志的形式,手动编码构建一个类似错误堆栈的日志信息,便于我们排查错误信息。

type error interface {
	Error() string
}

解放我们的代码

既然这种编码方式是为了在程序发生错误的时候,能够造一个异常堆栈,那么能不能通过直接打印err就能打印出异常堆栈呢?

原生的Go语言为了保持语言的简洁性,并没有提供,我们可以借助三方库,为我们构造一个这样的堆栈,这个库就是https://github.com/pkg/errors

errors库的使用

安装errors
go get github.com/pkg/errors
errors包装原生err生成带异常堆栈的error

errors提供了很多跟Go原生错误的api相同的方法,如:errors.New,error.Errorf等,我们的方法在程序在发生错误的时候,如果调用了其他三方方法,我们可以通过errors.WithStack(err)包装这个方法,得到新的err对象是会携带错误的异常堆栈。

在程序发生错误的时候,不再使用原生Go为我们提供的构建错误的方法,而是替换成https://github.com/pkg/errors的库即可。

如这里有A方法调用B方法B方法调用C方法。那么我们只需要在方法调用的最底层方法,将错误使用errors进行包装即可。代码示例:

func A() error {
    if err:=B(); err != nil {
        return err
    }
    // do somthing 
    return nil
}

func B() error {
    if err:= C(); err!=nil {
        return err
    }
    // do somthing 
    return nil
}

func C() error {
    //todo  somthing err
    return errors.New("C error")
}

然后我们如果想打印错误堆栈信息,只需要通过%+v的形式即可获取

if err != nil {
    log.Errorf("%+v",err)
}

此时,日志就会记录下错误的异常堆栈信息。

C error
go_test.C
        /Users/Desktop/go_test/err_test.go:27
go_test.B
        /Users/Desktop/go_test/err_test.go:19
go_test.A
        /Users/Desktop/go_test/err_test.go:11
go_test.TestErr
        /Users/Desktop/go_test/err_test.go:30
testing.tRunner
        /usr/local/go/src/testing/testing.go:1439
runtime.goexit
        /usr/local/go/src/runtime/asm_amd64.s:1571

使用errors需要注意的问题

  1. 只能包装一次堆栈信息,包装多次会打印多次重复堆栈

func A() error {
	if err := B(); err != nil {
		return errors.WithStack(err)
	}
	// do some thing
	return nil
}

func B() error {
	if err := C(); err != nil {
		return errors.WithStack(err)
	}
	return nil
}

func C() error {
	//todo  somthing err
	return errors.New("C error")
}
func TestErr(t *testing.T) {
	err := A()
	if err != nil {
		fmt.Printf("%+v", err)
	}
}

打印结果:

C error
go_test.C
        /Users/Desktop/go_test/err_test.go:27
go_test.B
        /Users/Desktop/go_test/err_test.go:19
go_test.A
        /Users/Desktop/go_test/err_test.go:11
go_test.TestErr
        /Users/Desktop/go_test/err_test.go:30
testing.tRunner
        /usr/local/go/src/testing/testing.go:1439
runtime.goexit
        /usr/local/go/src/runtime/asm_amd64.s:1571
go_test.B
        /Users/Desktop/go_test/err_test.go:20
go_test.A
        /Users/Desktop/go_test/err_test.go:11
go_test.TestErr
        /Users/Desktop/go_test/err_test.go:30
testing.tRunner
        /usr/local/go/src/testing/testing.go:1439
runtime.goexit
        /usr/local/go/src/runtime/asm_amd64.s:1571
go_test.A
        /Users/Desktop/go_test/err_test.go:12
go_test.TestErr
        /Users/Desktop/go_test/err_test.go:30
testing.tRunner
        /usr/local/go/src/testing/testing.go:1439
runtime.goexit
        /usr/local/go/src/runtime/asm_amd64.s:1571

所以这里的建议是方法的源头进行包装,其他地方直接返回错误就可以啦~