golang error 处理最佳实践#
错误类型定义#
1
2
3
| type error interface {
Error() string
}
|
基本上,error
是实现该接口任何内容,它将错误消息作为字符串返回。
构造错误#
可以使用 Go 的内置或包动态构造错误。
例如,以下函数使用包返回带有静态错误消息的新错误:errors fmt errors
1
2
3
4
5
6
7
| package main
import "errors"
func DoSomething() error {
return errors.New("something didn't work")
}
|
同样,该包可用于向错误添加动态数据。
例如:fmt int string error
1
2
3
4
5
6
7
8
9
10
| package main
import "fmt"
func Divide(a, b int) (int, error) {
if b == 0 {
return 0, fmt.Errorf("can't divide '%d' by zero", a)
}
return a / b, nil
}
|
请注意,当用于用格式动词包装另一个错误时,这将非常有用 fmt.Errorf %w
在上面的示例中,还有其他一些重要事项需要注意。
- 错误可以返回为
nil
,它是 Go 中 error
的默认值或零值
。这很重要,因为检查是确定是否遇到错误的惯用方法(替换您可能在其他编程语言中熟悉的 / 语句)。if err != nil
- 错误通常作为函数中的最后一个参数返回。因此,在上面的示例中,我们按该顺序返回
int
和 nil
。 - 当我们返回错误时,函数返回的其他参数通常作为其默认的
零值
返回。函数的用户可能期望,如果返回非nil
错误,则返回的其他参数不相关。 - 最后,错误消息通常以小写形式编写,不以标点符号结尾。但是可以例外,例如,当包含专有名词,以大写字母开头的函数名称等。
定义预期错误#
Go
中的另一个重要技术是定义预期的错误,以便可以在代码的其他部分中显式检查它们。当遇到某种类型的错误时需要执行不同的代码分支时,这将非常有用。
关于 errors.Is()
和 errors.As()
#
如果我们想检查给定错误是否与另一个特定错误匹配,我们需要使用包中Is()
的函数errors
。如果我们对错误是否属于给定类型感兴趣,我们应该调用该As()
函数。errors.Is()
errors.Is()
功能#
此函数适合没有经过包装的错误
在下面的示例中,我们可以看到该函数validateInput
为badInput
. 此错误ErrBadInput
包含在由fmt.Errorf()
.
使用该Is(err, target error) bool
函数,我们可以检测到ErrBadInput
它是否被包装,因为该函数检查包装错误链中的任何错误是否与目标匹配。
因此,这种形式应该比if err == ErrBadInput
更可取。
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
| package main
import (
"errors"
"fmt"
)
const badInput = "abc"
var ErrBadInput = errors.New("bad input")
func validateInput(input string) error {
if input == badInput {
return fmt.Errorf("validateInput: %w", ErrBadInput)
}
return nil
}
func main() {
input := badInput
err := validateInput(input)
if errors.Is(err, ErrBadInput) {
fmt.Println("bad input error")
}
}
|
输出:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
| package main
import (
"errors"
"fmt"
)
var ErrorString1 = errors.New("test b")
func main() {
err := func() error {
return fmt.Errorf("I am %w", ErrorString1)
}()
if errors.Is(err, ErrorString1) {
fmt.Println("err b")
}
}
|
输出:
errors.As()
功能#
此函数将错误进行了包装
与 类似Is()
,As(err error, target interface{}) bool
检查包装错误链中的任何错误是否与目标匹配。不同之处在于此函数检查错误是否具有特定类型,不像Is()
,它检查它是否是特定的错误对象。
因为As
考虑到整个错误链,它应该比类型断言更可取if e, ok := err.(*BadInputError); ok
。
target函数的参数``As(err error, target interface{}) bool
应该是指向错误类型的指针,在这种情况下是*BadInputError
复制
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
| package main
import (
"errors"
"fmt"
)
const badInput = "abc"
type BadInputError struct {
input string
}
func (e *BadInputError) Error() string {
return fmt.Sprintf("bad input: %s", e.input)
}
func validateInput(input string) error {
if input == badInput {
return fmt.Errorf("validateInput: %w", &BadInputError{input: input})
}
return nil
}
func main() {
input := badInput
err := validateInput(input)
var badInputErr *BadInputError
if errors.As(err, &badInputErr) {
fmt.Printf("bad input error occured: %s\n", badInputErr)
}
}
|
定义哨兵错误#
基于前面的函数,我们可以通过预先定义Sentinel
错误来改进错误信令。调用函数可以使用以下命令显式检查此错误:errors.Is
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
| package main
import (
"errors"
"fmt"
)
var ErrDivideByZero = errors.New("divide by zero")
func Divide(a, b int) (int, error) {
if b == 0 {
return 0, ErrDivideByZero
}
return a / b, nil
}
func main() {
a, b := 10, 0
result, err := Divide(a, b)
if err != nil {
switch {
case errors.Is(err, ErrDivideByZero):
fmt.Println("divide by zero error")
default:
fmt.Printf("unexpected division error: %s\n", err)
}
return
}
fmt.Printf("%d / %d = %d\n", a, b, result)
}
|
定义自定义错误类型#
使用上述策略可以涵盖许多错误处理用例,但是,有时您可能需要更多功能。也许您希望错误携带其他数据字段,或者错误的消息在打印时应使用动态值填充自身。这些可以通过自定义错误类型实现
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
36
37
38
39
40
41
42
| import (
"errors"
"fmt"
)
type DivisionError struct {
IntA int
IntB int
Msg string
}
func (e *DivisionError) Error() string {
return e.Msg
}
func Divide(a, b int) (int, error) {
if b == 0 {
return 0, &DivisionError{
Msg: fmt.Sprintf("cannot divide '%d' by zero", a),
IntA: a, IntB: b,
}
}
return a / b, nil
}
func main() {
a, b := 10, 0
result, err := Divide(a, b)
if err != nil {
var divErr *DivisionError
switch {
case errors.As(err, &divErr):
fmt.Printf("%d / %d is not mathematically valid: %s\n",
divErr.IntA, divErr.IntB, divErr.Error())
default:
fmt.Printf("unexpected division error: %s\n", err)
}
return
}
fmt.Printf("%d / %d = %d\n", a, b, result)
}
|
error are value#
无论你做什么,都要始终检查你的错误!
bufio
的设计#
1
2
3
4
5
6
7
8
| scanner := bufio.NewScanner(input)
for scanner.Scan() {
token := scanner.Text()
// process token
}
if err := scanner.Err(); err != nil {
// process the error
}
|
1
| func (s *Scanner) Scan() (token []byte, error)
|
1
2
3
4
5
6
7
8
| scanner := bufio.NewScanner(input)
for {
token, err := scanner.Scan()
if err != nil {
return err // or maybe break
}
// process token
}
|
文件error
处理#
改进前
1
2
3
4
5
6
7
8
9
10
11
12
13
| _, 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
}
// and so on
|
改进方法1
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
| 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])
// and so on
if ew.err != nil {
return ew.err
}
|
改进方法2(bufio
的设计)
1
2
3
4
5
6
7
8
| b := bufio.NewWriter(fd)
b.Write(p0[a:b])
b.Write(p1[c:d])
b.Write(p2[e:f])
// and so on
if b.Flush() != nil {
return b.Flush()
}
|
Error handling and Go#
合理的对错误进行包装,给错误一个模板(参考定义预期错误)
简化重复的错误处理#
原始程序
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
| func init() {
http.HandleFunc("/view", viewRecord)
}
func viewRecord(w http.ResponseWriter, r *http.Request) {
c := appengine.NewContext(r)
key := datastore.NewKey(c, "Record", r.FormValue("id"), 0, nil)
record := new(Record)
if err := datastore.Get(c, key, record); err != nil {
http.Error(w, err.Error(), 500)
return
}
if err := viewTemplate.Execute(w, record); err != nil {
http.Error(w, err.Error(), 500)
}
}
|
封装函数对错误处理
1
2
3
4
5
6
7
8
9
10
11
12
13
| func viewRecord(w http.ResponseWriter, r *http.Request) error {
c := appengine.NewContext(r)
key := datastore.NewKey(c, "Record", r.FormValue("id"), 0, nil)
record := new(Record)
if err := datastore.Get(c, key, record); err != nil {
return err
}
return viewTemplate.Execute(w, record)
}
|
外层对重复错误的处理
1
2
3
4
5
| func (fn appHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if err := fn(w, r); err != nil {
http.Error(w, err.Error(), 500)
}
}
|
一个http
服务的错误封装#
1
2
3
4
5
| type appError struct {
Error error
Message string
Code int
}
|
1
| type appHandler func(http.ResponseWriter, *http.Request) *appError
|
1
2
3
4
5
6
7
| func (fn appHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if e := fn(w, r); e != nil { // e is *appError, not os.Error.
c := appengine.NewContext(r)
c.Errorf("%v", e.Error)
http.Error(w, e.Message, e.Code)
}
}
|
1
2
3
4
5
6
7
8
9
10
11
12
| func viewRecord(w http.ResponseWriter, r *http.Request) *appError {
c := appengine.NewContext(r)
key := datastore.NewKey(c, "Record", r.FormValue("id"), 0, nil)
record := new(Record)
if err := datastore.Get(c, key, record); err != nil {
return &appError{err, "Record not found", 404}
}
if err := viewTemplate.Execute(w, record); err != nil {
return &appError{err, "Can't display record", 500}
}
return nil
}
|
毛剑老师的课程的建议#
panic#
- 在程序启动的时候,如果有强依赖的服务出现故障时
panic
退出 - 在程序启动的时候,如果发现有配置明显不符合要求, 可以
panic
退出(防御编程) - 其他情况下只要不是不可恢复的程序错误,都不应该直接
panic
应该返回 error
- 在程序入口处,例如
gin
中间件需要使用 recover
预防 panic
程序退出 - 在程序中我们应该避免使用野生的
goroutine
- 如果是在请求中需要执行异步任务,应该使用异步
worker
,消息通知的方式进行处理,避免请求量大时大量 goroutine
创建 - 如果需要使用
goroutine
时,应该使用同一的 Go
函数进行创建,这个函数中会进行 recover
,避免因为野生 goroutine
panic 导致主进程退出
1
2
3
4
5
6
7
8
9
10
11
| func Go(f func()){
go func(){
defer func(){
if err := recover(); err != nil {
log.Printf("panic: %+v", err)
}
}()
f()
}()
}
|
error#
我们在应用程序中使用 github.com/pkg/errors
处理应用错误,注意在公共库当中,我们一般不使用这个
error
应该是函数的最后一个返回值,当error
不为nil
时,函数的其他返回值是不可用的状态,不应该对其他返回值做任何期待
func f() (io.Reader, *S1, error)
在这里,我们不知道 io.Reader
中是否有数据,可能有,也有可能有一部分
错误处理的时候应该先判断错误, if err != nil
出现错误及时返回,使代码是一条流畅的直线,避免过多的嵌套.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
| // good case
func f() error {
a, err := A()
if err != nil {
return err
}
// ... 其他逻辑
return nil
}
// bad case
func f() error {
a, err := A()
if err == nil {
// 其他逻辑
}
return err
}
|
在应用程序中出现错误时,使用 errors.New
或者 errors.Errorf
返回错误
1
2
3
4
5
6
7
8
| func (u *usecese) usecase1() error {
money := u.repo.getMoney(uid)
if money < 10 {
errors.Errorf("用户余额不足, uid: %d, money: %d", uid, money)
}
// 其他逻辑
return nil
}
|
- 如果是调用应用程序的其他函数出现错误,请直接返回,如果需要携带信息,请使用
errors.WithMessage
1
2
3
4
5
6
7
8
9
| func (u *usecese) usecase2() error {
name, err := u.repo.getUserName(uid)
if err != nil {
return errors.WithMessage(err, "其他附加信息")
}
// 其他逻辑
return nil
}
|
- 如果是调用其他库(标准库、企业公共库、开源第三方库等)获取到错误时,请使用
errors.Wrap
添加堆栈信息- 切记,不要每个地方都是用
errors.Wrap
只需要在错误第一次出现时进行 errors.Wrap
即可 - 根据场景进行判断是否需要将其他库的原始错误吞掉,例如可以把
repository
层的数据库相关错误吞掉,返回业务错误码,避免后续我们分割微服务或者更换 ORM
库时需要去修改上层代码 - 注意我们在基础库,被大量引入的第三方库编写时一般不使用
errors.Wrap
避免堆栈信息重复
1
2
3
4
5
6
7
8
9
| func f() error {
err := json.Unmashal(&a, data)
if err != nil {
return errors.Wrap(err, "其他附加信息")
}
// 其他逻辑
return nil
}
|
- 禁止每个出错的地方都打日志,只需要在进程的最开始的地方使用
%+v
进行统一打印,例如 http/rpc
服务的中间件 - 错误判断使用
errors.Is
进行比较
1
2
3
4
5
6
7
8
9
| func f() error {
err := A()
if errors.Is(err, io.EOF){
return nil
}
// 其他逻辑
return nil
}
|
- 错误类型判断,使用
errors.As
进行赋值
1
2
3
4
5
6
7
8
9
10
11
| func f() error {
err := A()
var errA errorA
if errors.As(err, &errA){
// ...
}
// 其他逻辑
return nil
}
|
- 如何判定错误的信息是否足够,想一想当你的代码出现问题需要排查的时候你的错误信息是否可以帮助你快速的定位问题,例如我们在请求中一般会输出参数信息,用于辅助判断错误
- 对于业务错误,推荐在一个统一的地方创建一个错误字典,错误字典里面应该包含错误的 code,并且在日志中作为独立字段打印,方便做业务告警的判断,错误必须有清晰的错误文档
- 不需要返回,被忽略的错误必须输出日志信息
- 同一个地方不停的报错,最好不要不停输出错误日志,这样可能会导致被大量的错误日志信息淹没,无法排查问题,比较好的做法是打印一次错误详情,然后打印出错误出现的次数
- 对同一个类型的错误,采用相同的模式,例如参数错误,不要有的返回 404 有的返回 200
- 处理错误的时候,需要处理已分配的资源,使用
defer
进行清理,例如文件句柄
panic or error#
- 在 Go 中 panic 会导致程序直接退出,是一个致命的错误,如果使用
panic
,recover
进行处理的话,会存在很多问题- 性能问题,频繁
panic
, recover
性能不好 - 容易导致程序异常退出,只要有一个地方没有处理到就会导致程序进程整个退出
- 不可控,一旦 panic 就将处理逻辑移交给了外部,我们并不能预设外部包一定会进行处理
- 什么时候使用 panic 呢?
- 对于真正意外的情况,那些表示不可恢复的程序错误,例如索引越界、不可恢复的环境问题、栈溢出,我们才使用 panic
- 使用 error 处理有哪些好处?
- 简单。
- 考虑失败,而不是成功(Plan for failure, not success)。
- 没有隐藏的控制流。
- 完全交给你来控制 error。
- Error are values。
参考链接#