Go 编程模式:错误处理
Go语言的错误处理
Go 语言的函数支持多返回值,所以,可以在返回接口把业务语义(业务返回值)和控制语义(出错返回值)区分开。Go 语言的很多函数都会返回 result、err 两个值。
- 参数上基本上就是入参,而返回接口把结果和错误分离,使得函数的接口语义清晰;
- Go 语言中的错误参数如果要忽略,需要显式地忽略,用 _ 这样的变量来忽略;
- 因为返回的 error 是个接口(其中只有一个方法 Error(),返回一个 string ),所以可以扩展自定义的错误处理。
如果一个函数返回多个不同类型的error,可采用如下方式:
if err != nil {
switch err.(type) {
case *json.SyntaxError:
...
case *ZeroDivisionError:
...
case *NullPointerError:
...
default:
...
}
}
Go语言的错误处理的方式,本质上是返回值检查,但它兼顾了异常的一些好处——对错误的扩展。
资源清理
Go语言使用defer关键词进行清理
func Close(c io.Closer) {
err := c.Close()
if err != nil {
log.Fatal(err)
}
}
func main() {
r, err := Open("a")
if err != nil {
log.Fatalf("error opening 'a'\n")
}
defer Close(r) // 使用defer关键字在函数退出时关闭文件。
r, err = Open("b")
if err != nil {
log.Fatalf("error opening 'b'\n")
}
defer Close(r) // 使用defer关键字在函数退出时关闭文件。
}
Error Check Hell
反面示例
func parse(r io.Reader) (*Point, error) {
var p Point
if err := binary.Read(r, binary.BigEndian, &p.Longitude); err != nil {
return nil, err
}
if err := binary.Read(r, binary.BigEndian, &p.Latitude); err != nil {
return nil, err
}
if err := binary.Read(r, binary.BigEndian, &p.Distance); err != nil {
return nil, err
}
if err := binary.Read(r, binary.BigEndian, &p.ElevationGain); err != nil {
return nil, err
}
if err := binary.Read(r, binary.BigEndian, &p.ElevationLoss); err != nil {
return nil, err
}
}
上述代码可以用函数式编程的方式进行优化
func parse(r io.Reader) (*Point, error) {
var p Point
var err error
read := func(data interface{}) {
if err != nil {
return
}
err = binary.Read(r, binary.BigEndian, data)
}
read(&p.Longitude)
read(&p.Latitude)
read(&p.Distance)
read(&p.ElevationGain)
read(&p.ElevationLoss)
if err != nil {
return &p, err
}
return &p, nil
}
这段代码通过使用 Closure 的方式把相同的代码给抽出来重新定义一个函数,这样大量的 if err!=nil 处理得很干净了,但是会带来一个问题,那就是有一个 err 变量和一个内部的函数,感觉不是很干净。
能不能搞得更干净一点呢?从 Go 语言的 bufio.Scanner()中似乎可以学习到一些东西:
scanner := bufio.NewScanner(input)
for scanner.Scan() {
token := scanner.Text()
// process token
}
if err := scanner.Err(); err != nil {
// process the error
}
scanner在操作底层的 I/O 的时候,那个 for-loop 中没有任何的 if err !=nil 的情况,退出循环后有一个 scanner.Err() 的检查,看来使用了结构体的方式。模仿它,就可以对我们的代码进行重构了。
首先,定义一个结构体和一个成员函数:
type Reader struct {
r io.Reader
err error
}
func (r *Reader) read(data interface{}) {
if r.err == nil {
r.err = binary.Read(r.r, binary.BigEndian, data)
}
}
然后代码就可以变成下面这样:
func parse(input io.Reader) (*Point, error) {
var p Point
r := Reader{r: input}
r.read(&p.Longitude)
r.read(&p.Latitude)
r.read(&p.Distance)
r.read(&p.ElevationGain)
r.read(&p.ElevationLoss)
if r.err != nil {
return nil, r.err
}
return &p, nil
}
包装错误
通常来说可以使用 fmt.Errorf()来完成这个事
if err != nil {
return fmt.Errorf("something failed: %v", err)
}
在 Go 语言的开发者中,更为普遍的做法是将错误包装在另一个错误中,同时保留原始内容:
type authorizationError struct {
operation string
err error // original error
}
func (e *authorizationError) Error() string {
return fmt.Sprintf("authorization failed during %s: %v", e.operation, e.err)
}
更好的方式是通过一种标准的访问方法,最好使用一个接口,比如 causer接口中实现 Cause() 方法来暴露原始错误,以供进一步检查:
type causer interface {
Cause() error
}
func (e *authorizationError) Cause() error {
return e.err
}
上述代码可以使用第三方的错误库取代
import "github.com/pkg/errors"
//错误包装
if err != nil {
return errors.Wrap(err, "read failed")
}
// Cause接口
switch err := errors.Cause(err).(type) {
case *MyError:
// handle specifically
default:
// unknown error
}
极客时间版权所有: https://time.geekbang.org/column/article/330207