0%

Go微服务实战03-golang错误处理

1. error

Go的处理异常逻辑是不引入exception,支持多参数返回,所以你很容易的在函数签名中带上实现了error interface的对象,交由调用者来判定。

如果一个函数返回了(value,eror),你不能对这个value做任何假设,必须先判定error。唯一可以忽略error的是,如果你连value也不关心。

1.1 error 和 panic

使用多个返回值和一个简单的约定,Go解决了程序员知道什么时候出了问题,并为真正的异常情况保留了panic。

对于真正意外的情况,那些表示不可恢复的程序错误,例如索越界”不可恢复的环境问题、栈溢出,我们才使用panic。对于其他的错误情况,我们应该是期望使用erorr 来进行判定。

1.2 减少 if error(结构体包含error)

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
31
32
33
34
35
// 优化前
_, err = fd.Write(p0[a:b])
if err != nil {
return err
}
_, err = fd.Write(p1[c:d])
if err != nil {
return err
}
_, err = fd.Write(p2[e:f])
if err != nil {
return err
}



// 优化后
type errWriter struct {
w io.Writer
err error
}
func (ew *errWriter) write(buf []byte) {
if ew.err != nil {
return
}
_, ew.err = ew.w.Write(buf)
}
ew := &errWriter{w: fd}
ew.write(p0[a:b])
ew.write(p1[c:d])
ew.write(p2[e:f])
if ew.err != nil {
return ew.err
}

1.3 wrap error(推荐)

参考 “github.com/pkg/errors”

你应该只处理错误一次。处理错误意味着检查错误值,并做出单一决定。我们经常发现类似的代码,在错误处理中,带了两个任务:记录日志并且再次返回错误。

日志记录与错误无关且对调试没有帮助的信息应被视为噪音,应予以质疑。记录的原因是因为某些东西失败了,而日志包含了答案。

注意:要么打印它,要么往上抛。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 原始错误保留,并带上自定义的信息,Wrap还带有堆栈信息。

// Wrap returns an error annotating err with a stack trace
// at the point Wrap is called, and the supplied message.
// If err is nil, Wrap returns nil.
func Wrap(err error, message string) error {
if err == nil {
return nil
}
err = &withMessage{
cause: err,
msg: message,
}
return &withStack{
err,
callers(),
}
}

使用

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
package main

import (
"fmt"
"os"

"github.com/pkg/errors"
)

func main() {
_, err := os.Open("dsds.tx")
werr := errors.Wrap(err, "open file fail")
fmt.Printf("err: %T %v\n", errors.Cause(werr), errors.Cause(werr))
fmt.Printf("err: %+v", werr)
}


/*
err: *fs.PathError open dsds.tx: no such file or directory

err: open dsds.tx: no such file or directory
open file fail
main.main
/Users/liuwei/go/src/golangTest/main.go:12
runtime.main
/usr/local/go/src/runtime/proc.go:271
runtime.goexit
/usr/local/go/src/runtime/asm_arm64.s:1222
*/

使用技巧

  1. 你的应用代码中,使用errors.New 或者 erros.Errorf返回错误。
1
2
3
4
5
6
7
8
9
10
package main

import (
"github.com/pkg/errors"
)

func main() {
errors.New("1234") // 多了堆栈信息
}

  1. 如果调用其他包内的函数,通常简单的直接返回
1
2
3
if err != nil {
return err
}
  1. 如果和标准库协作,考虑errors.wrap保存堆栈信息
  2. 直接返回错误,而不是每个错误产生的地方到处打日志。
  3. 在程序的顶部或者是工作的goroutine 顶部(请求入口),使用%+v把堆栈详情记录。

注意问题

  1. 如果你的基础库,不应该用wrap包装,如果别人再包装,会有多个堆栈信息。只建议在业务库使用。

  2. 如果错误处理不了,尽量包装足够多的上下文信息,帮助上层排查。

  3. 如果错误被处理了,不应该再往上抛,应该return nil。

1.4 标准库 errors.Is / As (go 1.13)

新增 %w 谓词 包装错误。

1
2
3
4
5
6
// 包装
err := fmt.Errorf("access denied:%w",ErrPermission)

// 使用
if errors.Is(err,ErrPermission){
}

用%w包装错误可用于errors.ls以及errors.As。

2. error 方式

2.1 sentinel Error(避免)

预定义的特定错误,我们叫为sentine error,这个名字来源于计算机编程中使用一个特定值来表示不可能进行进一步处理的做法。所以对于Go,我们使用特定的值来表示错误。

缺点:

  1. 使用sentinel值是最不灵活的错误处理策略,因为调用方必须使用==将结果与预先声明的值进行比较。当您想要提供更多的上下文时,这就出现了一个问题,因为返回一个不同的错误将破坏相等性检查。
  2. Sentinel errors成为你API公共部分。如果您的公共函数或方法返回一个特定值的错误,那么该值必须是公共的,当然要有文档记录,这会增加API的表面积。
  3. sentinel errors最糟糕的问题是它们在两个包之间创建了源代码依赖关系。例如,检查错误是否等于io.EOF,您的代码必须导入io包。
  4. 尽可能避免哨兵错误。

2.2 error types (避免)

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
31
32
33
34
// 定义
type MyError struct {
Msg string
File string
Line int
}

func (e *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}
}


// 使用

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


// go 1.13
var e *MyError
if errors.As(err, &e) {
// handle error
}

一个不错的例子就是 os.PathError。调用者要使用类型断言和类型switch,就要让自定义的error变为public。这种模型会导致和调用者产生强耦合,从而导致API变得脆弱。

然错误类型比sentinel errors更好,因为它们可以捕获关于出错的更多上下文。

结论是尽量避免使用error types,或者避免将它们作为公共API的一部分。

2.3 opaque errors(鼓励)

Opaque errors 可以被认为是隐性错误。这是因为当错误发生时,你无法取得该错误的类型,无法完成类型诊断。标准库中有不少这样的实例,例如网络操作的超时错误等。之所以无法取得错误类型,原因在于相应的错误类型是 non-exported 的。

我将这种风格称为不透明错误处理,因为虽然您知道发生了错误,但您没有能力看到错误的内部。作为调用者,关于操作的结果,您所知道的就是它起作用了,或者没有起作用(成功还是失败)。

https://go.dev/src/net/net.go

1
2
3
4
5
6
7
8
9
type temporary interface {
Temporary() bool
}

// 在不导入包的情况下可以直接使用err相关行为
func IsTemporary(err error) bool {
te, ok := err.(temporary)
return ok && te.Temporary()
}

拿到网络请求返回的 error 后,调用 IsTemporary 函数,如果返回 true,那就重试。

3. 参考资料

可以加首页作者微信,咨询相关问题!