Golang中巧妙的错误处理

149 阅读3分钟

Go语言中会出现 满屏的 if err != nil 该怎么办

Java中的错误处理

Java 语言使用 try-catch-finally 通过使用异常的方式来处理错误,使用抛异常和抓异常的方式可以让我们的代码有这样一些好处。

  • 函数接口在 input(参数)和 output(返回值)以及错误处理的语义是比较清楚的
  • 正常逻辑的代码可以跟错误处理和资源清理的代码分开,提高了代码的可读性
  • 异常不能被忽略(如果要忽略也需要 catch 住,这是显式忽略)
  • 在面向对象的语言中(如 Java),异常是个对象,所以,可以实现多态式的 catch。
  • 与状态返回码相比,异常捕捉有一个显著的好处,那就是函数可以嵌套调用,或是链式调用

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语言错误处理代码:

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
    }
}

最终我们可以将代码优化成以下方式



package main

import (
  "bytes"
  "encoding/binary"
  "fmt"
)

// 长度不够,少一个Weight
var 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 错误
}

包装错误

通常来说,我们会使用 fmt.Errorf()来完成

if err != nil {
   return fmt.Errorf("something failed: %v", err)
}

更为普遍的做法是将错误包装在另一个错误中,同时保留原始内容


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 specifically
default:
    // unknown error
}