爆肝3天只为Golang 错误处理最佳实践

Posted 文大侠666

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了爆肝3天只为Golang 错误处理最佳实践相关的知识,希望对你有一定的参考价值。

对于开发者来说,要是不爽Go错误处理,那就看看最佳实践。Go可能引入try catch吗?那可能估计有点难度。本文简单介绍Go为什么选择这样的错误处理和目前常见处理方式,并梳理常见Go错误处理痛点,给出最佳实践,相信看完本文你会觉得Go错误处理好像也没那么糟糕,甚至好像还挺自然。

错误处理前世今生

关于错误处理,目前就两种方式

  1. 函数返回值判断,参考C语言
  2. try-catch-finally结构化异常处理,参考Java

当前主流语言都是选择结构化异常处理方式,结构化异常处理的优势在于

  1. 专注于业务处理逻辑,所有错误处理放在catch集中处理
  2. 提供了一套业务处理框架,相当于沉淀业务最佳实践,什么时候处理异常(catch)什么时候处理清理回收(finally)各个语言都差不多,快速上手

理想是丰满的,但现实是骨感的,如果你用过C++/Java异常处理,那么想想遇到多少无脑try catch的场景。

  • 什么?发生错误了,加个try catch看看
  • 为什么会出错?管它呢,加个try catch看看
  • 错误在哪里发生的?管它呢,包个大try catch
  • 抓到异常怎么办?懒得处理,先catch起来
  • 异常时怎么清理?先不考虑!

既然是错误处理,那么使用方应该明确知道是否可能有错误发生,错误在哪里发生,发生时应该怎么处理,这个逻辑应该是透明的。try catch的简单性很大一部分是因为鼓励开发者做不精确的错误处理思考,托管了部分错误处理流程,为开发者兜底结果开发确实方便很多,由此带来的错误也不少,往往程序员的过度滥用造成的后果往往可能比处理错误本身更严重

  • 无脑try catch带来性能损耗
  • 该处理的错误没有被正确处理或者忽略
  • 出现bug时,很难定位错误的发生点

大道至简

基于此,Go更鼓励开发者明确知道错误的处理过程,所谓大道至简就是对于整个过程开发者知道自己在干啥,不要企图依赖错误处理来兜底自己的开发错误
当然,并不能说Go的错误处理方式更优,只能说相对可控性强一点。事实上对于业务处理,肯定是try catch更方便,毕竟相当于编译器帮开发者做了很多处理工作,大多数时候我们只想一把梭,只要代码写的快,bug就追不上我,这也为什么Go用于业务开发常被抱怨的原因。

最佳实践

事实上,关于错误处理,Go官方也一直在迭代,也给出一些最佳实践。总的原则就是,Go的错误应该被当做值,既然是值,那么错误处理的方式完全取决于开发者,推荐一些最佳实践(套路),躺平的理直气壮。
当然,现在官方也一直在讨论,这部分最佳实践该怎么沉淀成语法。但Go的卖点就在足够简单,所以对于如何不破坏简单性并增强功能上很慎重,官方宁可先搞点最佳实践,开发者比着用用,也拒绝过早承诺和引入没想好的特性。

常见处理方式

常用处理方式足以应付大多数简单开发场景,比如一个简单的cli工具等等。

简单处理

函数中一般使用errors.New和fmt.Errorf,上层if err != nil 判断是否发生错误,对于简单的封装调用,这种方式即可。

// 简单处理
func funcA() error 
	// do something
	return errors.New("funcA error")
    // return fmt.Errorf("funcB error %d", 1)


func TestSimple(t *testing.T) 
	err := funcA()
	if err != nil 
		t.Logf("err %v", err)
		return
	

标准错误匹配判断

类似 try catch,提前定义好不同的标准Error,统一调用可能返回不同的错误,上游调用针对不同类型不同处理
简单的可直接判断类型


// 分支判断
var (
	ErrA = errors.New("A error")
)

func funcA2() error 
	// do something
	if true 
		return ErrA
	

	// do something
	return nil


func TestSimple2(t *testing.T) 
	err := funcA2()
	if err == ErrA 
		t.Logf("err %v", err)
		return
    

复杂多分支处理,通过switch匹配判断,如下


// 分支判断
var (
	ErrA = errors.New("A error")
	ErrB = errors.New("B error")
	ErrC = errors.New("C error")
)

func funcB2(param int) error 
	if param == 0 
		return ErrA
	 else if param == 1 
		return ErrB
	 else if param == 2 
		return ErrC
	

	// do something
	return nil


func TestSimple2(t *testing.T) 
	err := funcB2(0)
	if err != nil 
		switch err 
		case ErrA:
			//..
			return
		case ErrB:
			//...
			return
		case ErrB:
			//..
			return
		default:
			//...
			return
		
	

自定义错误匹配判断

对于需要复杂逻辑的Error或屏蔽底层细节的需求,我们可以实现自定义的error,实现如下接口即可。

type error interface 
	Error() string

返回结果,通过类型匹配做分支判断

// 自定义错误
type ErrMyA struct 
	Param string


func (e *ErrMyA) Error() string 
	return fmt.Sprintf("invalid param: %+v", e.Param)


type ErrMyB struct 
	Param string


func (e *ErrMyB) Error() string 
	return fmt.Sprintf("invalid param: %+v", e.Param)


// 函数调用
func funcB3(param int) error 
	if param == 0 
		return &ErrMyA"A"
	 else if param == 1 
		return &ErrMyB"B"
	

	// do something
	return nil


// 类型匹配判断
func TestSimple3(t *testing.T) 
	err := funcB3(0)
	if v, ok := err.(*ErrMyA); ok 
		t.Logf("err %v", v)
		return
	

	err = funcB3(0)
	if err != nil 
		switch err.(type) 
		case *ErrMyA:
			//..
			return
		case *ErrMyB:
			//...
			return
		default:
			//...
			return
		
	

最佳实践

error定义的包依赖

无论是标准Error的列表,还是自定义错误的define声明,检测错误导致了两个包(package)之间产生代码级的依赖。比如,检查某个错误是否是 io.EOF(预定义Error),不得不依赖 io 包;检查某个错误是否为自定义ErrA,不得不依赖ErrA的定义。
事实上,理想的情况是:代码实现上最好可以不用 import 定义该错误的包,从而导致的耦合,毕竟调用方只关心错误的行为,并不关系底层实现细节,这也就是所谓的透明型错误(Opaque errors)。

  • 透明型错误检测

这里参考Dave Cheney的方法,要想上层不依赖下层错误定义,最简单的就是上层只判断是否出错,压根不关心具体信息。如下

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

为了支持不同错误的具体信息判断怎么做呢?Golang的接口是鸭子类型,只需要通过预定义接口,在上层调用中判断是否实现了某个接口而即可。注意,接口定义需要两个文件都要。看起来能解决问题,但是实际中增加使用复杂度,很少用。

type temporary interface 
        Temporary() bool

 
// IsTemporary returns true if err is temporary.
func IsTemporary(err error) bool 
        te, ok := err.(temporary)
        return ok && te.Temporary()

  • 独立定义和声明

实际上,实际业务中前一种方式用得很少。
目前最广泛使用的,特别是微服务中,就是单独定义一个错误包,项目组统一维护,其它相关使用方引入该包使用即可

业务上下文输出

error除了输出错误之外,往往我们需要输出当时的相关业务信息,比如业务模块/错误码/错误消息等等,最佳实践是同一项目,在基础error基础上封装一套统一的自定义error。目前各个业务实现中最常用。
参考如下,

  • 定义业务相关信息,这里简单定义错误码和消息,一般单独模块维护
// 单独保存的业务error code和msg等

const (
	ErrSuccess   = 0
	ErrInvalid   = 1
	ErrWrongUser = 2
)

var code2msg = map[int]string
	ErrSuccess:   "Success",
	ErrInvalid:   "Invalid Param",
	ErrWrongUser: "Wrong User",


// 独立的自定义error
type MyError struct 
	mod  string
	code int
	msg  string

  • 然后自定义MyError,自定义业务信息输出和记录,可以在此基础上增加信息和方法实现
// 独立的自定义error
type MyError struct 
	mod  string
	code int
	msg  string


func NewMyError(mod string, code int) *MyError 
	return &MyError
		mod,
		code,
		code2msg[code],
	


func (e *MyError) Error() string 
	return fmt.Sprintf("Error detail: %+v %+v %+v", e.mod, e.code, e.msg)


func (e *MyError) Code() int 
	return e.code


func (e *MyError) Msg() string 
	return e.msg


func doSomethingA() error 
	return NewMyError("A", ErrSuccess)


func doSomethingB() error 
	return NewMyError("B", ErrInvalid)

  • 最后实际使用中记录和输出业务相关
func TestDoSomething(t *testing.T) 
	err := doSomethingA()

	if err != nil 
		// 分错误处理
		switch err.(type) 
		case *MyError:

			// 分业务处理
			myerr := err.(*MyError)
			switch myerr.Code() 
			case ErrSuccess:
				// ...
				t.Logf("MyError - %v", err)
			case ErrInvalid:
				// ...
			case ErrWrongUser:
				// ...
			

		default:
			t.Log("Other error")
		
	

到处可见的err!=nil

Go 错误处理最大特点恐怕就是满屏飘的if err != nil。典型的代码调用,如下

// 简单处理
func funcA() error 
	// do something
	return errors.New("funcA error")


func funcB() error 
	// do something
	return fmt.Errorf("funcB error %d", 1)


func funcC() error 
	// do something
	return fmt.Errorf("funcC error %d", 2)


func TestSimple(t *testing.T) 
	err := funcA()
	if err != nil 
		t.Logf("err %v", err)
		return
	

	err = funcB()
	if err != nil 
		t.Logf("err %v", err)
		return
	

	err = funcC()
	if err != nil 
		t.Logf("err %v", err)
		return
	

关于如何优化,Rob Pike给了两种典型优化和处理套路。

  • 嵌套函数

计算机中没什么是加一层不能解决的,这里引入嵌套函数——一次run过程中,每一步都过统一的err检查,最后做统一的err判断处理

// 简单处理
func funcAA() error 
	// do something
	return errors.New("funcA error")


func funcBB() error 
	// do something
	return errors.New("funcB error")


func funcCC() error 
	// do something
	return errors.New("funcC error")


func TestSimpleTidy(t *testing.T) 

	// 外包函数判断
	var err error
	callFunc := func(f func() error) 
		if err != nil 
			return
		

		err = f()
	

	// 顺序调用
	callFunc(funcAA)
	callFunc(funcBB)
	callFunc(funcCC)

	// 统一判断
	if err != nil 
		t.Logf("Error - %v", err)
	


  • 嵌套接口实现

和嵌套函数实现类似,这里接口封装定义的结构体保存了错误处理相关信息,对外暴露信息少,而且很容易实现链式调用和错误处理,如下:

type WorkRunner struct 
	err error


func NewWorkRunner() *WorkRunner 
	return &WorkRunner


func (w *WorkRunner) run(f func() error) 
	if w.err == nil 
		w.err = f()
	


func (w *WorkRunner) funcAA() *WorkRunner 
	// do something
	w.run(func() error 
		return errors.New("funcA error")
	)
	return w


func (w *WorkRunner) funcBB() *WorkRunner 
	// do something
	w.run(func() error 
		return errors.New("funcB error")
	)
	return w


func (w *WorkRunner) funcCC() *WorkRunner 
	// do something
	w.run(func() error 
		return errors.New("funcC error")
	)
	return w


func TestSimpleTidy2(t *testing.T) 

	// 对象统一管理判断
	w := NewWorkRunner()

	// 顺序链式调用
	w.funcAA().funcBB().funcCC()

	// 统一判断
	if w.err != nil 
		t.Logf("Error - %v", w.err)
	

跟踪错误堆栈

在Go应用里,一个逻辑往往要经多多层函数的调用才能完成,那在程序里我们的建议Error Handling 尽量留给上层的调用函数做,中间和底层的函数通过错误包装把自己要记的错误信息附加再原始错误上再返回给外层函数

期望的功能:

  • 错误包装(Wrap)和解包装(UnWarp),返回的是一个error堆栈(Stack)
  • 可以打印error堆栈(Stack)
  • 被包装的error无法直接=或type判断是否为具体错误类型,需要支持error堆栈中查找判断

Go 1.13中常依赖github.com/pkg/errors,Go 1.13后官方引入类似机制处理。

  • 官方库

Go 1.13后推荐直接使用官方库。
如下是一个服务请求过程模拟,control->service->dao->db逐级调用,返回原始error或自定义error。
扩展fmt.Errorf__函数,使用__%w__来生成包装错误

var ErrDbOrigin = errors.New("ErrDbOrigin")

type ErrDbDefine struct 
	info string


func (e ErrDbDefine) Error() string 
	return fmt.Sprintf("Error detail: %+v ", e.info)


func controlFunc(param interface) error 
	if err := serviceFunc(param); err != nil 
		return fmt.Errorf("error when controlFunc...: [%w]", err)
	

	return nil


func serviceFunc(param interface) error 
	if err := daoFunc(param); err != nil 
		return fmt.Errorf("error when serviceFunc...: [%w]", err)
	

	return nil


func daoFunc(param interface) error 
	if err := dbFunc(param); err != nil 
		return fmt.Errorf("error when daoFunc...: [%w]", err)
	

	return nil


func dbFunc(param interface) error 
	// do something
	return ErrDbOrigin
	//return ErrDbDefine"ErrDbOrigin error info"

UnWrap逐级解包装,fmt.Print直接输出error堆栈

fmt.Printf("error -> %v\\n", err)
fmt.Printf("error unwrap -> %v\\n", errors.Unwrap(err))

输出如下

error -> error when controlFunc…: [error when serviceFunc…: [error when daoFunc…: [ErrDbOrigin]]]
error unwrap -> error when serviceFunc…: [error when daoFunc…: [ErrDbOrigin]]

包装的后的原始error和自定义eror分别使用Is/As判断

		// 无法匹配
		if err == ErrDbOrigin 
			fmt.Printf("error print with equal -> %v\\n", err)
		

		if errors.Is(err, ErrDbOrigin) 
			fmt.Printf("error print with Is -> %v\\n", err)
		

		// 无法匹配
		if v, ok := err.(ErrDbDefine); ok 
			fmt.Printf("error print with type -> %v\\n", v)
		

		var b ErrDbDefine
		if errors.As(err, &b) 
			fmt.Printf("error print with As -> %v\\n", err)
		
  • github库

相对官方库,github提供的方法更多,支持更多细节,整体差不多,目前很多地方还在使用。
提供多种Wrap方式来包装错误

var ErrDbOrigin2 = errors.New("ErrDbOrigin")

type ErrDbDefine2 struct 
	info string


func (e ErrDbDefine2) Error() string 
	return fmt.Sprintf("Error detail: %+v ", e.info)


func controlFunc2(param interface) error 
	if err := serviceFunc2(param); err != nil 
		return giterrors.Wrap(err, "error when controlFunc")
	

	return nil


func serviceFunc2(param interface) error 
	if err := daoFunc2(param); err != nil 
		return giterrors.Wrap(err, "error when serviceFunc")
	

	return nil


func daoFunc2(param interface) error 
	if err := dbFunc2(param); err != nil 
		retu

以上是关于爆肝3天只为Golang 错误处理最佳实践的主要内容,如果未能解决你的问题,请参考以下文章

爆肝3天只为Golang 错误处理最佳实践

Go中的错误和异常处理最佳实践

Django ajax 错误响应最佳实践

Java 基础语法爆肝1W字只为弄懂类和对象

这是编写以在javascript / node js中处理错误处理then-catch或try-catch的最佳实践[重复]

在线程中处理在 catch 块中抛出的异常的最佳实践。 (。网)