Error

  • Go error 就是一个普通的接口,普通的值
1
2
3
type error interface {
    Error() string
}
  • go 选择此种方法处理异常的优势

    • 简单
    • 考虑失败,而不是成功
    • 没有隐藏的控制流
    • 完全交给开发者控制 error
    • Error are values
  • 异常处理注意到点

    • 异常每次只应该被处理一次,即不应该调用 print 并 return
    • 错误要被日志记录
    • 应用程序处理错误,保证 100% 完整性,应返回完整信息
    • 处理后不再报告当前错误

使用方式

直接定义 error 类型

  • 使用 error.New 直接返回自定义异常信息
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
if err != nil {
    return nil, errors.New("sql run failed:" + q)
}
// PS:errors.New()返回的实际上是个地址,因此进行等值判定时不能作为简单的字符串判定
func New(text string) error {
    return &errorString{text}
}

type errorString struct {
    s string
}

存在问题

  • 很难对异常的类型进行判定,无法拦截异常类型进行特定处理

预定义的特定错误(sentinel errors)

  • 提前定义异常类型信息
1
2
3
4
5
6
// io 库一些全局异常变量
var ErrShortWrite = errors.New("short write")
var ErrShortBuffer = errors.New("short buffer")
var EOF = errors.New("EOF")
var ErrUnexpectedEOF = errors.New("unexpected EOF")
var ErrNoProgress = errors.New("multiple Read calls return no data or error")
  • 可根据异常类型进行判定,进行后续操作
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
for {
    _, err = br.ReadString('\n')
    if err != nil {
        break
    }
    lines++
}
if err != io.EOF {
    return 0, err
}

存在问题

  • 不够灵活,由于需要与预定义的异常类型进行等值判定,所以无法再次对异常进行封装,无法携带更多信息,否则破坏想等性检查
    • 不推荐依赖 error.Error 的输出,这个方法仅用于调试或输出异常至控制台或日志,而不能将其作为字符串进行异常类型的匹配
  • Sentinel errors 成为了 API 的公共部分
    • 会增加包暴露的表面积,调用者在进行调用时,如果要进行异常类型的判定,那么这个异常一定是公共的,而且要有文档记录
    • 如果 API 定义了一个返回特定错误的 interface,那么该接口的所有实现都将被限制为仅返回该错误,即使他们可以提供描述更具体的错误
  • Sentinel errors 在两个包之间创建了依赖
    • 如检查错误是否等于 io.EOF,就必须倒入 io 包

结论

  • 尽可能避免 Sentinel errors
  • 虽然标准库中有大量的使用,但是个人开发库时,应尽量减少避免

Error type

  • 自定义一个异常类型,以包含更多的上下文信息
1
2
3
4
5
6
7
8
9
// 定义异常类型
type PathError struct {
    Op   string
    Path string
    Err  error
}

// 返回 error string
func (e *PathError) Error() string { return e.Op + " " + e.Path + ": " + e.Err.Error() }
  • 使用时可对异常类型进行断言,以针对异常进行具体的处理
  • 同时又可以携带更多上下文信息
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
allPeople, err := lunchDB.GetAllDepPeople(firstDeptId)
if err != nil {
    switch err := err.(type) {
    case nil:
        log.Println("query success")
    case *QueryError:
        log.Println("获取部门所有用户失败", err)
    default:
        log.Println("unknown error")
    }
}

问题

  • 虽然解决了 Sentinel errors 无法携带更多上下文信息的问题,但是增大了暴露包的表面积等问题仍没有解决

结论

  • 尽可能避免使用,至少在公共 api 和公共包内尽量不要使用

不透明的异常处理(Opaque errors)

  • 是最灵活的异常处理方式,代码和调用者之间耦合最少
  • 虽然知道发生了错误,但是没有能力看到错误的内部。作为调用者,关于操作的结果,只知道他是起作用了或是没起作用
  • 调用者只需要返回错误,而不假设其他内容
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
// 调用者并不关心 error 的内容,而是直接向上层抛
func (aliOss *AliOSS) ClientBucket() error {
    client, err := oss.New(aliOss.Endpoint, aliOss.AccessKeyId, aliOss.AccessKeySecret)
    if err != nil {
        return err
    }
    aliOss.BucketClient, err = client.Bucket(aliOss.BucketName)
    if err != nil {
        return err
    }
    return nil
}
  • 在部分情况下,二分的错误方法是不够的,在这种情况下,可以断言错误类型实现了特定的行为,而不是断言错误是特定类型或特定的值
1
2
3
4
5
6
7
8
type temporary interface {
    Temporary() bool
}

func IsTemporary(err error) bool {
    te, ok := err.(temporary)
    return ok && te.Temporary()
}

Wrap errors

  • Wrap 的方式可以包装更多的错误信息,只在最外层处理一次
  • 同时又可以通过 解 Wrap 的方式获取根因,进行等值判定
  • 如果和其他库进行协作,考虑使用 errors.Wrap 或者 errors.Wrapf 保存堆栈信息。同样适用 于和标准库协作的时候。
  • 直接返回错误,而不是每个错误产生的地方到处打日志。

使用 golang 本身的 Wrap 功能

  • go 1.13后版本支持的处理方式
  • go1.13为 errors 和 fmt 标准库包引入了新特性,以简化处理包含其他错误的错误。其中最重要的是: 包含另一个错误的 error 可以实现返回底层错误的 Unwrap 方法。如果 e1.Unwrap() 返回 e2,那么我们说 e1 包装 e2,您可以展开 e1 以获得 e2。
  • go1.13 errors 包包含两个用于检查错误的新函数:Is 和 As。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
func (m *DBModel) queryDepPeople1(q string, d int) ([]*DepPeople, error) {
    depPeoples := []*DepPeople{}
    query := fmt.Sprintf(q, d)

    err := m.DBEngine.QueryRow(query).Scan(&depPeoples)
    if err != nil {
        return nil, fmt.Errorf("query sql error: %s,%w", q, err)
    }

    return depPeoples, nil

}

func (m *DBModel) GetAllDepPeople(d int) ([]*DepPeople, error) {
    query := "SELECT ;"
    allPeople, err := m.queryDepPeople1(query, d)
    if err != nil && !errors.Is(err, sql.ErrNoRows) {
        return nil, fmt.Errorf("获取部门所有订餐用户失败: %w", err)
    }
    return allPeople, nil
}

github.com/pkg/errors

  • 能记录堆栈信息
  • 在程序的顶部或者是工作的 goroutine 顶部(请求入口),使用 %+v 把堆栈详情记录
  • 使用 errors.Cause 获取 root error,再进行和 sentinel error 判定
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
func (m *DBModel) queryDepPeople1(q string, d int) ([]*DepPeople, error) {
    depPeoples := []*DepPeople{}
    query := fmt.Sprintf(q, d)

    err := m.DBEngine.QueryRow(query).Scan(&depPeoples)
    if err != nil {
        // 封装异常并记录堆栈信息
        return nil, errors.Wrapf(err, "query sql error %s", q)
    }

    return depPeoples, nil
}

func (m *DBModel) GetAllDepPeople(d int) ([]*DepPeople, error) {
    query := "SELECT ;"
    allPeople, err := m.queryDepPeople1(query, d)
    if err != nil && !errors.Is(err, sql.ErrNoRows) {
        return nil, err
    }
    return allPeople, nil
}
1
2
3
4
5
6
7
func main(){
    allPeople, err := lunchDB.GetAllDepPeople(firstDeptId)
    if err != nil {
        // 打印堆栈信息
        fmt.Printf("%+v\n", err)
    }
}

使用建议

  • 只有业务代码才使用 Warp errors
    • 如果定义的那个库会被很多项目依赖,则不应该使用 Warp errors,因为会记录多次堆栈信息,应只返回根因
  • 果函数/方法不打算处理错误,那么用足够的上下文 Wrap errors 并将其返回到调用堆栈中
    • 例如,sql 查询场景下,额外的上下文可以是使用的输入参数或失败的查询语句
  • 一旦确定函数/方法处理了错误,错误就不再是错误。
    • 如果函数/方法仍然需要发出返回,则它不能返回错误。它应该只返回 nil (比如降级处理中,你返回了降级数据,然后需要 return nil)。

书写技巧

错误优先判定,优先返回

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
// 推荐
func main() {
    _, err := os.Open("notExit.txt")
    if err != nil {
        // handle error
    }
    // do sth
}

// 不推荐
func main() {
    _, err := os.Open("notExit.txt")
    if err == nil {
        // do sth
    }
    // handle error
}

方法调用函数只返回 error 时可写在一行

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
// 推荐
func (aliOss *AliOSS) PutObjectFromFile(objectName, localFilePath string) error {
    return aliOss.BucketClient.PutObjectFromFile(objectName, localFilePath)
}

// 不推荐
func (aliOss *AliOSS) PutObjectFromFile(objectName, localFilePath string) error {
    err := aliOss.BucketClient.PutObjectFromFile(objectName, localFilePath)
    if err != nil {
        return err
    }
    return nil
}

使用函数内置 Scan() 和 Err(),简化代码

  • 需注意 return 时返回 Err(),否则会忽略掉 Scan() 一半时抛出的错误
  • 包内其他方法处理操作时,先判定 sc.Err() 是否为空,如果不为空则直接 return error ,不进行任何后续操作
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
// 推荐
func CountLines(r io.Reader) (int, error) {
    sc := bufio.NewScanner(r)
    lines := 0

    for sc.Scan() {
        lines++
    }

    return lines, sc.Err()
}

// 不推荐
func CountLines(r io.Reader) (int, error) {
    var (
        br    = bufio.NewReader(r)
        lines int
        err   error
    )
    for {
        _, err = br.ReadString('\n')
        if err != nil {
            break
        }
        lines++
    }
    if err != io.EOF {
        return 0, err
    }
    return lines, err

}
  • 进行类结构体声明时,预留一个 err 字段,处理过程中异常写入 err,执行最后,调用 Err() 方法判断执行过程中是否有报错
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
type Scanner struct {
    r            io.Reader // The reader provided by the client.
    split        SplitFunc // The function to split the tokens.
    maxTokenSize int       // Maximum size of a token; modified by tests.
    token        []byte    // Last token returned by split.
    buf          []byte    // Buffer used as argument to split.
    start        int       // First non-processed byte in buf.
    end          int       // End of data in buf.
    err          error     // Sticky error.
    empties      int       // Count of successive empty tokens.
    scanCalled   bool      // Scan has been called; buffer is in use.
    done         bool      // Scan has finished.
}

func (s *Scanner) Scan() bool {
    for {
        // do sth
        for loop := 0; ; {
            n, err := s.r.Read(s.buf[s.end:len(s.buf)])
            if n < 0 || len(s.buf)-s.end < n {
                s.setErr(ErrBadReadCount)
                break
            }
            s.end += n
            if err != nil {
                // 调用 setErr ,写入 s 中
                s.setErr(err)
                break
            }
            if n > 0 {
                s.empties = 0
                break
            }
            loop++
            if loop > maxConsecutiveEmptyReads {
                s.setErr(io.ErrNoProgress)
                break
            }
        }
    }
}

func (s *Scanner) setErr(err error) {
    if s.err == nil || s.err == io.EOF {
        s.err = err
    }
}

// 存在非 EOF 的 error 则返回
func (s *Scanner) Err() error {
    if s.err == io.EOF {
        return nil
    }
    return s.err
}

参考