Go 语言的错误处理 | 青训营笔记

91 阅读2分钟

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 语言的错误处理的方式,本质上是返回值检查,但是它也兼顾了异常的一些好处——对错误的扩展。

资源清理

出错后是需要做资源清理的,不同的编程语言有不同的资源清理的编程模式。

C 语言:使用的是 goto fail; 的方式到一个集中的地方进行清理.

C++ 语言:一般来说使用 RAII 模式,通过面向对象的代理模式,把需要清理的资源交给一个代理类,然后再析构函数来解决。

Java 语言:可以在 finally 语句块里进行清理。

Go 语言:使用 defer 关键词进行清理。

下面是一个 Go 语言的资源清理的示例:

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

说到 Go 语言的 if err !=nil 的代码了,这样的代码的确是能让人写到吐。那么有没有什么好的方式呢?有的。我们先看一个令人崩溃的代码。

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 处理得很干净了。