龙空技术网

GO 编程:错误处理

奇幻小鱼k 207

前言:

而今大家对“c语言的错误处理机制”可能比较珍视,同学们都想要分析一些“c语言的错误处理机制”的相关资讯。那么小编同时在网络上收集了一些对于“c语言的错误处理机制””的相关文章,希望看官们能喜欢,看官们一起来学习一下吧!

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 关键词进行清理。

下面是一个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关键字在函数退出时关闭文件。}

好了,说到 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 处理的很干净了。但是会带来一个问题,那就是有一个 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}

有了上面这个技术,我们的“流式接口 Fluent Interface”,也就很容易处理了。如下所示:

package mainimport (  "bytes"  "encoding/binary"  "fmt")// 长度不够,少一个Weightvar b = []byte {0x48, 0x61, 0x6f, 0x20, 0x43, 0x68, 0x65, 0x6e, 0x00, 0x00, 0x2c} var r = bytes.NewReader(b)type Person struct {  Name [10]byte  Age uint8  Weight uint8  err error}func (p *Person) read(data interface{}) {  if p.err == nil {    p.err = binary.Read(r, binary.BigEndian, data)  }}func (p *Person) ReadName() *Person {  p.read(&p.Name)   return p}func (p *Person) ReadAge() *Person {  p.read(&p.Age)   return p}func (p *Person) ReadWeight() *Person {  p.read(&p.Weight)   return p}func (p *Person) Print() *Person {  if p.err == nil {    fmt.Printf("Name=%s, Age=%d, Weight=%d\n",p.Name, p.Age, p.Weight)  }  return p}func main() {     p := Person{}  p.ReadName().ReadAge().ReadWeight().Print()  fmt.Println(p.err)  // EOF 错误}

相信你应该看懂这个技巧了,但是,其使用场景也就只能在对于同一个业务对象的不断操作下可以简化错误处理,对于多个业务对象的话,还是得需要各种 if err != nil的方式。

包装错误

最后,多说一句,我们需要包装一下错误,而不是干巴巴地把err给返回到上层,我们需要把一些执行的上下文加入。

通常来说,我们会使用 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}

这里有个好消息是,这样的代码不必再写了,有一个第三方的错误库(github.com/pkg/errors),对于这个库,我无论到哪都能看到他的存在,所以,这个基本上来说就是事实上的标准了。代码示例如下:

import"github.com/pkg/errors"//错误包装if err != nil{   return errors.Wrap(err, "read failed")}// Cause接口switch err := errors.Cause(err).(type){case *MyError:  // handle specificallydefault:  // unknown error}

标签: #c语言的错误处理机制