基本认识
- 在Go中,将错误当成值来进行处理,强调判断错误和处理错误,不支持
try/catch
捕获异常。
- Go选择使用
Error
而非Exception
来进行错误处理。
- 一般把错误作为函数或方法的最后一个返回值。
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) 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个常用的方法:
New
:创建一个新的 error
func Is(err, target error) bool
:判断err是否包含target,是不是特定的某个error
func As(err error, target interface{}) bool
:判断error是否为target类型,类型转换为特定的error(用得不多)
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{} wrappedErr := fmt.Errorf("this is an wrapped error %w", err)
if err == errors.Unwrap(wrappedErr) { fmt.Println("unwrapped") } if errors.Is(wrappedErr, err) { fmt.Println("wrapped is err") }
copyErr := &MyError{} if errors.As(wrappedErr, ©Err) { fmt.Println("convert error") } }
|
panic
意味着fatal error
,调用者不能解决,彻底结束。可能遇到的场景:
- 调用别人的代码,别人没有合理使用
panic
(自己写代码还是用error
)。
- 数组越界、不可恢复的环境、栈溢出等错误。
从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("这里将不会执行下来") }
|
使用原则
- 遇事不决选 error
- 当怀疑可以用 error 的时候,就说明不需要 panic
- 一般情况下,只有快速失败的过程,才会考虑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 { }
f, err := os.Open(path) if err == nil { }
|
少写if err != nil的技巧
- 返回
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 }
func AuthenticateRequest(r *Request) error { return authenticate(r.User) }
|
- 用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 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的数量,但是并没有从根本上解决不能提供详细上下文的问题。一方面,破坏原始错误,担心上层调用的人用做等值判定,只能一层层向上透传,最终输出没有堆栈没有上下文的信息,令人崩溃;另一方面,又想包装更多详细错误信息。
error.Wrap()
:保留原始错误信息,捎带一些附加信息。
errors.Cause()
:用来获取原始错误(根因,root error)。
errors.WithMessage()
:不保存堆栈信息。
实际应用时:
- 自己的应用代码中,使用
errors.New()
或者errors.Errorf()
返回错误;
- 如果调用其他包内的函数,直接返回,往上抛,不要在错误的地方到处打日志。(满足原则:只处理一次。)
- 如果使用三方库/标准库,使用
errors.Wrap()
或errors.Wrapf()
保存堆栈信息。
- 程序的顶部或者工作的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
,函数返回的结果可能是正确的,但是其他返回值的内容是错误的。
那么应该如何记录日志?原则:
- 错误要被日志记录
- *应用程序处理错误,保证**100%*完整性
- 之后不再报告当前错误
结合上一小节,包装错误的原则:
- 如果你提供的库很多人使用,不应该使用任何wrap包装错误
- 如果你的函数无法处理错误,携带足够多的上下文,用wrap.Errors往上抛(足够的上下文:能帮助解决问题,一般是什么人调用了什么接口,返回成功还是失败)
- 如果这个错误被处理过,就不要再抛了。
【参考资料】
- https://www.liwenzhou.com/posts/Go/error/