Golang通常有三种错误处理方式:错误哨兵(Sentinel Error)、错误类型断言和记录错误调用栈。错误哨兵指的是用特定值的变量作为错误处理分支的判定条件。错误类型用于路由错误处理逻辑,和错误哨兵有异曲同工的作用,由类型系统来提供错误种类的唯一性。错误黑盒指的是不过分关心错误类型,将错误返回给上层;当需要采取行动时,要针对错误的行为进行断言,而非错误类型。

本教程操作环境:windows7系统、GO 1.18版本、Dell G3电脑。

try-catch
func ReturnError() (string, error) {
	return "", fmt.Errorf("Test Error")
}

func main() {
	val, err := ReturnError()
	if err != nil {
		panic(err)
	}
	fmt.Println(val)
}
error

Golang通常有如下的三种错误处理方式,错误哨兵(Sentinel Error)、错误类型断言(Error Type Asseration)和记录错误调用栈。

错误哨兵(Sentinel Error)

gorm.RecordNotFoundedredis.NIL
error
var ErrTest = errors.New("Test Error")

err := doSomething()
if err == ErrTest{
	// TODO: Do With Error
}

使用哨兵存在如下几个问题存在两个问题:

==!=
var ErrTest1 = errors.New("ErrTest1")
var ErrTest2 = errors.New("ErrTest1")
var ErrTest3 = errors.New("ErrTest1")
……
var ErrTestN = errors.New("ErrTestN")
……
if err  == ErrTest1{
	……
} else if err == ErrTest2{
	……
}else if err == ErrTest3{
	……
}
……
else err == ErrTestN{
	……
}

2、哨兵变量值不能被修改,否则会导致逻辑错误,上述golang写法的error哨兵可以被改变,可以通过如下方式解决:

type Error string

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

3、哨兵变量会导致极强的耦合性,接口新增error的吐出就会造成使用者相应修改代码新增的处理错误问题。

相比较上面的方案,错误哨兵还有一种更为优雅的方案,依赖于接口而非变量:

var ErrTest1 = errors.New("ErrTest1")

func IsErrTest1(err error) bool{
  return err == ErrTest1
}

错误类型

错误类型来路由错误处理逻辑,和错误哨兵有异曲同工的作用,由类型系统来提供错误种类的唯一性,使用方法如下:

type TestError {
}
func(err *TestError) Error() string{
	return "Test Error"
}
if err, ok := err.(TestError); ok {
	//TODO 错误分支处理
}

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

使用接口抛出更复杂,多样的错误,依旧需要改变调用方的代码。

错误黑盒(依赖错误接口)

错误黑盒指的是不过分关心错误类型,将错误返回给上层。当需要采取行动时,要针对错误的行为进行断言,而非错误类型。

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

func main(){
	err := fn()
	if IsTemporary(err){
		fmt.Println("Temporary Error")
	}
}

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()
}

通过这样的方式,1.直接就解耦了接口间的依赖,2. 错误处理路由和错误类型无关,而与具体行为有关,避免了膨胀的错误类型。

总结

错误哨兵和错误类型避免不了依赖过重的问题,只有错误黑盒能够将问题从确定错误类型(变量)的处理逻辑变为确定错误行为。因此推荐使用第三种方式来处理错误。

Wrap
func authenticate() error{
	return fmt.Errorf("authenticate")
}

func AuthenticateRequest() error {
	err := authenticate()
	// OR logger.Info("authenticate fail %v", err)
	if err != nil {
		return errors.Wrap(err, "AuthenticateRequest")
	}
	return nil
}

func main(){
	err := AuthenticateRequest()
	fmt.Printf("%+v\n", err)
	fmt.Println("##########")
	fmt.Printf("%v\n", errors.Cause(err))
}

// 打印信息
authenticate
AuthenticateRequest
main.AuthenticateRequest
	/Users/hekangle/MyPersonProject/go-pattern/main.go:17
main.main
	/Users/hekangle/MyPersonProject/go-pattern/main.go:23
runtime.main
	/usr/local/Cellar/go@1.13/1.13.12/libexec/src/runtime/proc.go:203
runtime.goexit
	/usr/local/Cellar/go@1.13/1.13.12/libexec/src/runtime/asm_amd64.s:1357
##########
authenticate