Go语言错误处理方式

错误定义方式

1. Sentinel errors

​ 在很多包中甚至标准库中都会见到以errors.New() 方法获取一个错误并把它赋值为一个包级别的常量,如io.EOF , 这种特定的错误是API在某些场景下需要返回一个特定的错误interface 即使有更具描述性的错误可以返回。

​ 这样做会使包之间的源代码产生依赖关系,如检查错误是否是io.EOF, 项目必须导入io包。这样做很常见但是如果你的项目中很多包都要导出错误值,那么其他包必须依赖这些错误值才能做特定的错误检查,代码极度耦合。

​ 所以尽量避免出现这样错误处理

2. Error types

​ 把error处理成自定义的Type,这样做可以在error中添加一些上下文信息,以及更多的描述性信息。

1
2
3
4
5
6
7
8
9
10
11
12
13
type MyError struct{
Msg string
File string
Line int
}

func (m *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}
}
1
2
3
4
5
6
7
8
9
switch err := err.(type){
case nil:
// call succeded nothing to do
case *MyError:
fmt.Println("error occurred on line:",err.line)

default:
// unknown error
}

缺点:这种模型必须要暴露自定义的error类型供调用者调用,使得和调用者产生强耦合,使得API变得脆弱。调用者也必须要导入错误包。建议避免使用错误类型,至少便面把它作为公共API的一部分。

3. Error Interface

非透明的错误传递不会强依赖于包内变量,发现错误就向外抛出是最好的处理错误方式,但是如果外界要对包内错误进行判断就十分繁琐,不建议使用contain方法去强行匹配错误某些字段,这样很容易出现难以发现的bug。如何解决这一问题,我们可以定义一个行为而不是一个方式如:

1
2
3
4
5
6
7
8
type certErr interface{
ChainNotFind()bool
}

func IsChainNotFind(err error)bool{
ic,ok := err.(certErr)
return ok&ic.ChainNotFind()
}

这样不用暴露包内的错误类型与错误变量,可以直接通过方法来判断某个错误。减少强依赖。

错误处理方式

1. 错误直接抛出

不要使用err==nil 这样很容易使得代码无线缩进,建议在错误出现时直接抛出

2. 减少错误处理

使用多次调用某方法可以使用一个结构将err包裹,出错存储。这样在调用该方法时不用做错误处理

1
2
3
4
5
6
7
8
9
10
11
12
13
type errWriter struct{
io.Writer
err 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,nil
}

以上处理方式在bufio.Scan()中也有类似操作

3. 使用 pkg error 包

我们的代码经常会遇到错误一层层向上抛出,但是因为缺乏上下文描述信息导致错误无法追踪,处理这个问题要么是错误日志四处打印,要么是痛苦的debug。此时巧用pkg error包可以消除痛点。

这个库的灵魂是wrap, 最烦的也是wrap。

  1. 在应用代码中的自定义错误如长度过长,不符合正则,使用errors.new 或者 errors.Errorf 返回错误

  2. 如果调用项目中其他包内函数直接返回err

  3. 调用自己的基础库,第三方库,官方基础库使用errors.Wrap 或 errors.Wrapf

  4. 直接返回错误,不要打日志

  5. 在程序的顶部或是工作的goroutine的顶部(请求入口),使用%+v把堆栈详情记录下来

  6. 使用errors.Cause 获取root error 再和依赖库的sentienal error 去判定

  7. 如果是基础库(被很多项目使用的依赖第三方库,返回根错误),不要去进行wrap 只在应用中使用

4. 使用 go 1.3 的errors 包

  1. go 1.3 为errors 和 fmt 标准库添加了新的的使用方法,从而简化处理包含其他错误的错误。其中error 实现了一个 Unwrap 的方法,如果err1 包含 err2 使用Unwrap(err1) 返回err2
1
func (e *QueryError) Unwrap()error {return e.Err}
  1. go 1.3 还包含两个用于检查错误的新函数,Is ,As
1
2
3
// An error is considered to match a target if it is equal to that target or if
// it implements a method Is(error) bool such that Is(target) returns true.
func Is(err, target error) bool { return stderrors.Is(err, target) }

其实底层是不断地调用unwrap一旦错误相同就返回true。

1
2
3
// As will panic if target is not a non-nil pointer to either a type that implements
// error, or to any interface type. As returns false if err is nil.
func As(err error, target interface{}) bool { return stderrors.As(err, target) }

也是不断Unwrap ,一旦错误匹配就返回true 并把错误解析进相应的格式中。

  1. go 1.3 支持新的%w ,使用%w 的错误站位符可以把原始Error包入,支持Is, As
1
2
3
4
5
err := fmt.Errorf("access not support : %w",ErrAuth)

if errors.Is(err,ErrAuth){
...
}

example 1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30

type Error struct {
Path string
User string
}

func (e *Error)Error()string{
return fmt.Sprintf( "user : %s, and path is %s",e.User,e.Path)
}


func (e *Error)Is(target error)bool{
t,ok := target.(*Error)
if !ok{
return false
}

return (e.Path== t.Path || t.Path == "") && (e.User== t.User || t.User == "")
}

func api(){
err := dao()
if err !=nil{
if errors.Is(err,&Error{Path: "shanghai"}){
fmt.Print("error path failed is shanghai")
return
}
}
}

example 2

如果直接返回,会导致调用者依赖自定义错误,如果在ErrPermission中带了自定义字段,那么就无法判断,使用%w,则可加入字段,使用Is方法来判定

1
2
3
4
5
6
7
8
9
var ErrPermission = errors.New("permission denied")

func DoSomething()error{
if !userHasPermission(){
return fmt.Errorf("%w",ErrPermission)

}
}

5. go 1.3 结合 pkg error 使用

总结

  1. 错误判定不要强依赖。
  2. 错误日志只在顶层打印,不要四处打印
  3. 错误的堆栈信息只在错误第一次出现时存储,如第三方基础库调用时,sql语句出问题时。
  4. 尽量减少错误处理

根据这几点举个错误处理例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
import(
"errors"
xerrors "github.com/pkg/errors"

)


var errNotFind = errors.new("not find")

func main(){
err := test2()
fmt.Printf("main: %+v \n",err)
}


func test0()error{
return xerrors.Wrap(errNotFind,"test0 failed")
}

func test1()error{
return test0()
}

func test2()error{
return test1()
}

异常捕

go 语言的服务框架一般都自带recover 但在起go程并发时,野生go程的panic是无法捕获的,这经常会造成程序崩溃,推荐以下方式去recover

1
2
3
4
5
6
7
8
9
func Go(x func()){
defer func(){
if err := recover();if err != nil{
// log 日志
}
}()

go x()
}