赞
踩
Go 语言的错误处理是其经典设计,但是 Go 语言初学者往往为太多的错误处理感到困惑,为什么 Go 不能像其他语言一样可以异常捕获。
这其实是 Go 语言的设计哲学,在 Go 中错误就是值,需要显示的处理,这样代码才能更加健壮,开发人员才能对代码更加有信心。
我们不需要额外的语言机制去处理它们,而只需利用已有的语言机制,像处理其他普通类型值一样去处理错误。这也决定了这样的错误处理机制让代码更容易调试(就像对待普通变量值那样),也更容易针对每个错误处理的决策分支进行测试覆盖;同时,没有try-catch-finally的异常处理机制也让Go代码的可读性更佳。
要写出高质量的Go代码,我们需要始终想着错误处理。
Go 提供了两种构造错误值的方法,errors.New
和fmt.Errorf
:
err := errors.New("demo error info")
// 带有上下文信息的错误值
errWithCtx = fmt.Errorf("index %d is out of bounds", i)
wrapErr = fmt.Errorf("wrap error: %w", err)
Go 中一共有四种错误处理策略,不管是哪种错误处理策略都需要构造错误值:
err := doSomething()
if err != nil {
...
return err
}
当需要定义不同的错误类型,但是不需要携带错误上下文信息,根据不同错误类型,进行不同的处理时需要用到该模式。
哨兵错误值变量以 ErrXXX
格式命名。
优点
缺点
使用 Go 1.13 之后的版本,进行错误检视最好使用 Is 方法
:
package main import ( "errors" "fmt" ) var ErrSentinel = errors.New("the underlying sentinel error") func main() { err1 := fmt.Errorf("wrap err1: %w", ErrSentinel) err2 := fmt.Errorf("wrap err2: %w", err1) // 如果底层错误是一个包装错误,使用 Is 方法可以直接追溯到错误链的底层 if errors.Is(err2, ErrSentinel) { println("err is ErrSentinel") return } println("err is not ErrSentinel") }
该策略是哨兵策略的升级版,可以自定义错误类型,并且携带错误上下文信息。
如果使用的是 Go 1.13 之后的版本,在对自定义错误类型进行检视的时候最好使用 As 方法
package main import ( "errors" "fmt" ) type MyError struct { e string } func (e *MyError) Error() string { return e.e } func main() { var err = &MyError{"my error type"} err1 := fmt.Errorf("wrap err1: %w", err) err2 := fmt.Errorf("wrap err2: %w", err1) var e *MyError if errors.As(err2, &e) { println("err is a variable of MyError type ") println(e == err) return } println("err is not a variable of the MyError type ") }
As方法类似于通过类型断言判断一个error类型变量是否为特定的自定义错误类型:
// 类似 if e, ok := err.(*MyError); ok {...}
var e *MyError
if errors.As(err, &e) {
}
不同的是,如果error类型变量的底层错误值是一个包装错误,那么errors.As方法会沿着该包装错误所在错误链与链上所有被包装的错误的类型进行比较,直至找到一个匹配的错误类型。
将某个包中的错误类型归类,统一提取出一些公共的错误行为特征(behaviour),并将这些错误行为特征放入一个公开的接口类型中。
相较于前两种错误检视策略,该策略降低了错误处理方与错误构造方的耦合,同时又提供了错误类型。
例如 net 包下错误构造
下面是http包使用错误行为特征检视策略进行错误处理的代码:
Go 代码中会出现大量的 if err != nil
判断,有两种较好的方式优化,使代码可读性更高。
优化前代码如下:
利用panic和recover封装一套跳转机制,模拟实现一套check/handle机制。这样在降低复杂度的同时,也能在视觉呈现上有所改善。
package main import ( "fmt" "io" "os" ) func check(err error) { if err != nil { panic(err) } } func CopyFile(src, dst string) (err error) { var r, w *os.File // error handler defer func() { if r != nil { r.Close() } if w != nil { w.Close() } if e := recover(); e != nil { if w != nil { os.Remove(dst) } err = fmt.Errorf("copy %s %s: %v", src, dst, err) } }() r, err = os.Open(src) check(err) w, err = os.Create(dst) check(err) _, err = io.Copy(w, r) check(err) return nil } func main() { err := CopyFile("foo.txt", "bar.txt") if err != nil { fmt.Println("copyfile error:", err) return } fmt.Println("copyfile ok") }
将错误封装在结构体内部,在方法的入口判断错误是否为nil, 一旦为 nil 就不进行后续逻辑。
package main import ( "fmt" "io" "os" ) type FileCopier struct { w *os.File r *os.File err error } func (f *FileCopier) open(path string) (*os.File, error) { if f.err != nil { return nil, f.err } h, err := os.Open(path) if err != nil { f.err = err return nil, err } return h, nil } func (f *FileCopier) openSrc(path string) { if f.err != nil { return } f.r, f.err = f.open(path) return } func (f *FileCopier) createDst(path string) { if f.err != nil { return } f.w, f.err = os.Create(path) return } func (f *FileCopier) copy() { if f.err != nil { return } if _, err := io.Copy(f.w, f.r); err != nil { f.err = err } } func (f *FileCopier) CopyFile(src, dst string) error { if f.err != nil { return f.err } defer func() { if f.r != nil { f.r.Close() } if f.w != nil { f.w.Close() } if f.err != nil { if f.w != nil { os.Remove(dst) } } }() f.openSrc(src) f.createDst(dst) f.copy() return f.err } func main() { var fc FileCopier err := fc.CopyFile("foo.txt", "bar.txt") if err != nil { fmt.Println("copy file error:", err) return } fmt.Println("copy file ok") }
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。