Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[golang] 错误处理 #299

Open
wayou opened this issue Jul 22, 2021 · 0 comments
Open

[golang] 错误处理 #299

wayou opened this issue Jul 22, 2021 · 0 comments

Comments

@wayou
Copy link
Owner

wayou commented Jul 22, 2021

[golang] 错误处理

字符串类型的错误

// simple string-based error
err1 := errors.New("math: square root of negative number")

// with formatting
err2 := fmt.Errorf("math: square root of negative number %g", x)

自定义错误

自定义错误可携带额外信息,比如错误码等。

先定义一个错误码枚举:

// 定义枚举前需要先定义类型
type Status int

// 定义枚举
const (
	InvalidLogin = iota + 1
	NotFound
)

然后使用上面的枚举来实现自定义错误:

type StatusErr struct {
	Status  Status
	Message string
}

func (se StatusErr) Error() string {
	return se.Message
}

测试:

func foo() error {
	id := 1000
	return StatusErr{
		Status:  InvalidLogin,
		Message: fmt.Sprintf("status error with user id %d", id),
	}
}

func main() {
	err := foo()
	switch e := err.(type) {
	case StatusErr:
		fmt.Println("error with status code:", e.Status)
	default:
		fmt.Println(e)
	}
}

// error with status code: 1

上面示例代码中,注意两点:

  • 即使使用自定义的错误,也应该返回内置错误类型 error,这样调用方可以不依赖具体的错误类型,更加解耦
  • 当需要时,再还原原来的类型以获取其中的额外信息

哨兵错误

有一种错误,Sentinel 错误,约定以 Err 开头,定义在包作用域内,当调用方接收到该类型错误后,表示程序无需继续往下,应该终止执行。

比如标准库中解压时文件类型不对的错误 ErrorFormat:

func main() {
	data := []byte("blahblah...")
	notAZipFile := bytes.NewReader(data)
	_, err := zip.NewReader(notAZipFile, int64((len(data))))
	if err == zip.ErrFormat {
		fmt.Println("invalid file type!")
	}
}

查看标准库中的实现,可看到其就是普通的 error 类型,只是命名按照了上述约定:

// /xxx/go/1.16.5/libexec/src/archive/zip/reader.go

var (
	ErrFormat    = errors.New("zip: not a valid zip file")
	ErrAlgorithm = errors.New("zip: unsupported compression algorithm")
	ErrChecksum  = errors.New("zip: checksum error")
)

Error 的包装

接收到 error 后,可添加点信息在上面,再将其返回出去。这样可形成一条错误链。

前面涉及到的 fmt.Errorf 支持一个 %wwrap)参数,可接入一个 error 类型从而在其上面进行二次包装。

func bar() error {
	err := foo()
	return fmt.Errorf("some more info on err, the source err is :%w", err)
}

%v(value) 则只是将原错误信息的文本形成新文本,而不会有包装操作。

对应地,通过 errors.Unwrap 可解出被封装的原始错误信息:

func main() {
	err := bar()
	if err != nil {
		fmt.Println(err)
		if sourceErr := errors.Unwrap(err); sourceErr != nil {
			fmt.Println(sourceErr)
		}
	}
}

// some more info on err, the source err is :status error with user id 1000
// status error with user id 1000

自定义错误要支持 Unsrap 需要实现该方法:

	type StatusError struct {
		Status Status
		Message string
+		err error
	}
	
+	func (se StatusError) Unwrap()error{
+		return se.err
+	}

IsAs

既然错误可以二次包装,那么问题来了,如果对哨兵错误进行了包装,调用方如何知道错误中包含了哨兵错误呢。Go 中提供了两个方法 [errors.Is](http://errors.Is)[errors.As](http://errors.As) 来解决这个问题。

func fileChecker(name string) error {
	f, err := os.Open(name)
	if err != nil {
		return fmt.Errorf("in fileChecker: %w", err)
	}
	defer f.Close()
	return nil
}

func main() {
	err := fileChecker("xxx.txt")
	if err != nil {
		if errors.Is(err, os.ErrNotExist) {
			fmt.Println("file not exit")
		}
	}
}

内部实现上,errors.Is 是通过不断 Unwrap 错误并与之进行对比来判断的,所以,自定义错误要支持对比,需要实现 Is 方法:

type MyErr struct {
	Status int
}

func (me MyErr)Error()string{
	return fmt.Sprintf("error code: %d",me.Status)
}

func (me MyErr) Is(target error)bool{
	if me2,ok:=target.(MyErr);ok{
		return  reflect.DeepEqual(me,me2)
	}
	return false
}

As 判断一个错误是不是你要找的类型,该方法接收两个参数,一个是具体的错误,另一个是想要找的类型的一个指标,成功的话,说明在错误链上打到了对应类型错误并赋值给了这个指针。

func main() {
	err:=AFuncThatReturnsAnErr()
	var myErr MyErr
	if errors.As(err,&myErr){
		fmt.Println(myErr.Code)
	}
}

所以前面在讲自定义错误的时候,除了使用 err.(type) 来还原错误本身的类型,也可以用这里提到的 As

同理,也可以自己实现自定义错误的 As 方法来决定其中的细节,不过一般场景不涉及。

总结:

  • 使用 [errors.Is](http://errors.Is) 来查找具体某个错误值
  • 使用 [errors.As](http://errors.As) 来判断具体某个错误类型

结合 defer 来包装错误

函数中如果有多个地方都需要进行同样的错误包装,比如像下面这样:

func foo(i int)(string,error)  {
	_,err:=f1(i)
	if err=nil{
		return "",fmt.Errorf("in foo:%w",err)
	}
	_,err:=f2(i)
	if err=nil{
		return "",fmt.Errorf("in foo:%w",err)
	}
	_,err:=f3(i)
	if err=nil{
		return "",fmt.Errorf("in foo:%w",err)
	}
	return "",nil
}

联想到之前介绍过的 defer 可做一些收尾工作,可利用其在函数返回时来统一进行错误包装,优化掉这里的冗余代码:

func foo(i int)(_ string,err error)  {
	defer func() {
		if err!=nil{
			 err=fmt.Errorf("in foo:%w",err)
		}
	}()

	_,err:=f1(i)
	if err=nil{
		return "",err
	}
	_,err:=f2(i)
	if err=nil{
		return "",err
	}
	return f3(i)
}

注意使用 defer 进行返回时,需要为返回变量声明名称,这样才能在 defer 中使用。

发生 panic 及恢复

Go 程序中发生异常时会生成 panic,比如试图访问超出 slice 边界的元素,内存溢出等。

除了 runtime 会发生 panic,程序中也可根据需要来生成:

func genPanic(msg string) {
	panic(msg)
}

func main() {
	genPanic("blah")
}

运行结果:

$ go run main.go
panic: blah

goroutine 1 [running]:
main.genPanic(...)
        /xxx/main.go:4
main.main()
        /xxx/main.go:8 +0x50
exit status 2
make: *** [run] Error 1

可通过 recover 来处理 panic,结合 defer 来用,让程序优雅退出而不是崩溃:

func div60(i int) {
	defer func() {
		if v := recover(); v != nil {
			fmt.Println(v)
		}
	}()
	fmt.Println(60 / i)
}

func main() {
	for _, val := range []int{1, 2, 0, 6} {
		div60(val)
	}
}

运行结果:

go run main.go
60
30
runtime error: integer divide by zero
10

与其他语言的 try catch 不同,发生 panic 后不建议通过 recover 让程序还继续运行,而是需要根据 panic 进行对应善后。比如,如果是内存溢出,在 recover 时及时落日志上报监控保存现场然后通过 os.Exit(1) 退出程序。

不应该通过 panic recover 模式来获取出错的堆栈信息,recover 只是单纯提供一种在发生错误时,将信息打印出来,然后程序能够正常继续而不崩掉的机制。如果想打印出错误时的堆栈信息,一是可以通过包装错误自己实现,二是可用现成三方库。使用 fmt.Printf 时,也可通过 %+v 打印 stack trace,但注意此时会打印程序文件的绝对路径,真实生产环境中一般想要隐藏掉这些信息,可在编译时加上 -trimpath 参数。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant