基本认识

  1. 在Go中,将错误当成来进行处理,强调判断错误和处理错误,不支持try/catch捕获异常。
  2. Go选择使用Error而非Exception来进行错误处理。
  3. 一般把错误作为函数或方法的最后一个返回值

Error接口

使用error接口表示错误类型。该接口只有一个Error()方法,返回描述错误信息的字符串。

1
2
3
type error interface {
Error() string
}

接口类型的默认零值为nil,所以通常把调用函数时返回的错误和nil比较:

1
2
3
4
5
_, err := someFunc(some parameters)
if err != nil{
fmt.Println("出现错误:", err) // 使用标准库fmt打印错误自动调用error类型的Error方法,打印错误描述信息
return
}

Go这种机制的好处是,遇到error需要立即处理,而Java中是try/catch中包裹了一大堆代码,良性和致命的问题都会抛出错误,不容易排查问题。

创建错误

由于error是接口,可以自定义错误类型(开发中间件使用较多)。

最简单的创建错误的方法是用errors包提供的New函数创建一个错误:

1
2
3
func New(text string) error{
return &errorString{text} // 返回一个指针,使得每次返回都是一个新的对象,否则在做等值判断时可能会出问题。
}

错误的两种类型

error:可以被处理的错误;
panic:非常严重不可恢复的错误。

errors包

当需要传入格式化的错误描述信息,用fmt.Errorf更好,但是它提供很多描述错误的文本信息,会丢失原本的错误类型,导致错误在做等值判断时失效。为了解决这个缺陷,fmt.Errorf在1.13版本提供了特殊的格式化动词w%,可以基于已有错误再包装得到新的错误:

1
fmt.Errorf("查询数据库失败,err:%w", err)

对于这种二次包装的错误,errors包提供了4个常用的方法:

  1. New:创建一个新的 error
  2. func Is(err, target error) bool :判断err是否包含target,是不是特定的某个error
  3. func As(err error, target interface{}) bool:判断error是否为target类型,类型转换为特定的error(用得不多)
  4. func Unwrap(err error) error:获得error包含下一层错误,解除包装并返回被包装的 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
33
34
35
36
37
38
39
40
41
42
43
44
package main

import (
"errors"
"fmt"
)

func main() {
var err error = &MyError{}
println(err.Error())

ErrorsPkg()
}

type MyError struct {
}

func (m *MyError) Error() string {
return "Hello, it's my error"
}

func ErrorsPkg() {
err := &MyError{}
// 使用 %w 占位符,返回的是一个新错误
// wrappedErr 是一个新类型,fmt.wrapError
wrappedErr := fmt.Errorf("this is an wrapped error %w", err)

// 再解出来
if err == errors.Unwrap(wrappedErr) {
fmt.Println("unwrapped")
}

if errors.Is(wrappedErr, err) {
// 虽然被包了一下,但是 Is 会逐层解除包装,判断是不是该错误
fmt.Println("wrapped is err")
}

copyErr := &MyError{}
// 这里尝试将 wrappedErr转换为 MyError
// 注意我们使用了两次的取地址符号
if errors.As(wrappedErr, &copyErr) {
fmt.Println("convert error")
}
}

panic

意味着fatal error,调用者不能解决,彻底结束。可能遇到的场景:

  1. 调用别人的代码,别人没有合理使用panic(自己写代码还是用error)。
  2. 数组越界、不可恢复的环境、栈溢出等错误。

panic中恢复:

recover可以进行兜底,把这一次的request放弃,go的runtime会退出,可以去执行其他的request,但是风险比较大,revover一般就是记录个日志之类的,,示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package main

import "fmt"

func main() {
defer func() {
if data := recover(); data != nil {
fmt.Printf("hello, panic: %v\n", data)
}
fmt.Println("恢复之后从这里继续执行")
}()

panic("Boom")
fmt.Println("这里将不会执行下来")
}

使用原则

  1. 遇事不决选 error
  2. 当怀疑可以用 error 的时候,就说明不需要 panic
  3. 一般情况下,只有快速失败的过程,才会考虑panic

defer

用于在方法返回之前执行某些动作(类似于Java中的finally),一般用来释放资源(如锁等)。
执行顺序:像栈一样,先进后出。

处理错误

正常流程的代码

推荐的写法,err处理缩进,正常的代码是一条直线。

1
2
3
4
5
6
7
8
9
10
11
12
13
/////////推荐写法////////
f, err := os.Open(path)
if err != nil {
// handle error
}
// do stuff

//////////不推荐///////
f, err := os.Open(path)
if err == nil {
// do stuff
}
// handle error

少写if err != nil的技巧

  1. 返回err或者nil,可以直接return
1
2
3
4
5
6
7
8
9
10
11
12
13
//////原来的写法
func AuthenticateRequest(r *Request) error {
err := authenticate(r.User)
if err != nil {
return err
}
return nil
}

//////推荐的写法 //毕竟函数的返回值就要error类型,而且调用函数之后就返回一个error类型,那直接return就好了
func AuthenticateRequest(r *Request) error {
return authenticate(r.User)
}
  1. 用io.Reader统计读取内容的行数
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
//////原来的写法
func CountLines(r io.Reader) (int, error) {
var (
br = bufio.NewReader(r)
lines int
err error
)
for {
_, err = br.ReadString('\n')
lines++
if err != nil {
break
}
}
if err != io.EOF {
return 0, err
}
return lines, nil
}

//////推荐的写法 //
func CountLines(r io.Reader) (int, error) {
sc := bufio.NewScanner(r)
lines := 0
for sc.Scan() {
lines++
}
return lines, sc.Err()
}

利用bufio.Scanner方法,这个里面封装了按行读取的逻辑,并且其Scan方法读取时遇到错误会记录下来,最终通过 sc.Err()统一返回。

  1. 包装错误类型,缓存错误
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
56
57
58
////////////原来的写法
type Header struct {
Key, Value string
}

type Status struct {
Code int
Reason string
}

func WriteResponse(w io.Writer, st Status, headers []Header, body io.Reader) error {
_, err := fmt.Fprintf(w, "HTTP/1.1 %d %s\r\n", st.Code, st.Reason)
if err != nil {
return err
}

for _, h := range headers {
_, err := fmt.Fprintf(w, "%s: %s\r\n", h.Key, h.Value)
if err != nil {
return err
}
}

if _, err := fmt.Fprintf(w, "\r\n"); err != nil {
return err
}

_, err = io.Copy(w, body)
return err
}


//////////////推荐的写法 //
type errWriter struct {
io.Writer
err error // 用来暂存
}

func (e *errWriter) Write(buf []byte) (int, error) {
if e.err != nil {
return 0, e.err
}

var n int
n, e.err = e.Writer.Write(buf)
return n, e.err
}

func WriteResponse(w io.Writer, st Status, headers []Header, body io.Reader) error {
ew := &errWriter{Writer: w}
fmt.Fprintf(ew, "HTTP/1.1 %d %s\r\n", st.Code, st.Reason)
for _, h := range headers {
fmt.Fprintf(ew, "%s: %s\r\n", h.Key, h.Value)
}
fmt.Fprintf(ew, "\r\n")
io.Copy(ew, body)
return ew.err
}

下面这种写法,不用做任何err的判定,相当于在包装类里面复用了,更优雅。

使用errors包装错误-从根本上解决

上一小节只是减少了if err != nil的数量,但是并没有从根本上解决不能提供详细上下文的问题。一方面,破坏原始错误,担心上层调用的人用做等值判定,只能一层层向上透传,最终输出没有堆栈没有上下文的信息,令人崩溃;另一方面,又想包装更多详细错误信息。

  1. error.Wrap():保留原始错误信息,捎带一些附加信息。

  2. errors.Cause():用来获取原始错误(根因,root error)。

  3. errors.WithMessage():不保存堆栈信息。

实际应用时:

  1. 自己的应用代码中,使用errors.New()或者errors.Errorf()返回错误;
  2. 如果调用其他包内的函数,直接返回,往上抛,不要在错误的地方到处打日志。(满足原则:只处理一次。)
  3. 如果使用三方库/标准库,使用errors.Wrap()errors.Wrapf()保存堆栈信息。
  4. 程序的顶部或者工作的goroutine顶部,用%+v详细记录堆栈。

处理错误的原则

处理的原则是:如果遇到错误,只处理一次。

一些经常出现的错误代码,在错误处理中,既记录了日志,又返回了错误:

1
2
3
4
5
6
7
8
func WriteAll(w io.Writer, buf []byte) error {
_, err := w.Write(buf)
if err != nil {
log.Println("unable to write:", err)
return err
}
return nil
}

这个时候又尬住了,一方面,不记录日志,找不到是谁报错;另一方面,记录日志接下来调用者层层打印,在控制台的输出可能就层层割裂,没有完整的堆栈信息。

继续讲处理的原则:错误处理契约规定,出现错误时,不能对其他返回值的内容做任何假设。如果程序员忘记return,函数返回的结果可能是正确的,但是其他返回值的内容是错误的。

那么应该如何记录日志?原则:

  1. 错误要被日志记录
  2. *应用程序处理错误,保证**100%*完整性
  3. 之后不再报告当前错误

结合上一小节,包装错误的原则:

  1. 如果你提供的库很多人使用,不应该使用任何wrap包装错误
  2. 如果你的函数无法处理错误,携带足够多的上下文,用wrap.Errors往上抛(足够的上下文:能帮助解决问题,一般是什么人调用了什么接口,返回成功还是失败)
  3. 如果这个错误被处理过,就不要再抛了。

【参考资料】

  1. https://www.liwenzhou.com/posts/Go/error/