字节小喽喽
读完需要
速读仅需 3 分钟
Error vs Exception 错误和异常
首先简单介绍下 Go 生成 error 的基本方法:errors.New()。
Go 语言的 errors.New() 方法返回的是内部 errorString 对象的指针。两次调用 New 方法返回的对象在用 == 判断是否相等时会返回 false。errors.New 返回时最好在字符串信息中加上当前的包名。不取地址直接用 == 比较两个 go struct 会展开 struct 中的值作等值比较。
各种语言的错误处理
C 语言。单返回值,一般通过指针作为入参,通过返回值 int 表示成功或是失败。
C++。引入了 Exception,但是调用方不知道被调用方会抛出什么异常。Java。引入了 checked exception,此类 exception 调用者必须处理。方法的定义者会声明方法会抛出哪些异常。但是 Java 代码中往往使用 exception 异常趋于泛滥,难以区分良性的和致命的错误,启动程序时打印大片异常信息居然变成了一个正常现象。
Go 的 Error 处理
"You only need to check the error value if you care about the result."
有以下几个原则:
如果一个方法返回了 error,不能对返回值作任何假设,必须处理 error,除非不关心方法的处理结果。
go 的 panic 是 fatal error,是真正的异常,是不可处理的,表明真的出了问题,不能假设上层调用者会处理这个 panic;而 error 则是需要调用者进行处理的。
go 要求开发者即时处理错误,错误发生后,可以就地处理后不再向上层抛出,或是不处理向上层抛出。
Go 使用 Error 的优点
处理简单,不用像 Java 那样考虑多种 Exception 的处理,只要 error 不等于 nil 即发生了错误。
代码风格关注的是失败,而不是成功。要求开发者重点关注函数处理过程中返回的错误,而不是先假定代码会运行成功,之后再处理 Exception。
处理 Error 不会导致隐藏的控制流。处理 Error 时要求开发者即时处理 Error,在 Error 发生时就需要对 Error 进行处理。而不是像 try-catch 在代码 block 中各种跳转。
开发者拥有对 Error 的完全控制。可以处理或是忽略或是抛给上层。
Error are values。Error 在 Go 中即一个 Interface。https://blog.golang.org/errors-are-values ( https://blog.golang.org/errors-are-values )
Error Type in Golang
Go 语言中一些错误处理方式,包括使用哨兵错误或是使用错误 struct 类型等。
Sentinel Error 哨兵错误
即特殊的、预定义的某种错误。例如 golang 系统包定义的 io.EOF、底层的系统错误等。
使用 Sentinel Error 的一些缺点:
Sentinel errors 成为 API 的公共部分。因为方法返回的是 sentinel error,那么调用方就需要知道可能返回的 sentinel error 的所有类型。
Sentinel errors 在两个包之间创建了依赖。如果直接使用 sentinel errors 作等值判断,会依赖于包中预先定义的 error 类型。因此应该尽可能避免使用 Sentinel Errors。
Error Types 错误结构体类型
相比错误值,Error Types (即定义一个类似 MyError 这样的 struct 类型)可以带上更多的关于错误的上下文信息,通过一个结构体返回。但是使用 Error Types 还是会有使用 Sentinel Errors 同样的问题,因此同样不推荐使用。
Opaque Errors(推荐使用)
只需返回错误,而不假设其上下文。不直接断言错误的类型,而是判断是否实现了特定的行为。例如 net 包中的 net.Error 类型就实现了 IsTemporary 还有 IsTimeout 的两个方法,供调用方来判断 Error 是否有特定的行为。不暴露具体的 Error 类型,而是通过某个方法返回当前错误是否是某个错误的行为的信息,这样外部调用者就不需要知道具体的 Error 类型,减少包本身暴露的表面积。
Handling Error
使用 Go 编写代码处理相关代码时,缩进的应该是错误处理的代码,而不是正常流程的代码。
如何减少 Go 语言中 Error 处理的代码?
通过减少 Error 而减少 Error 处理代码
type errWriter struct {io.Writererr error}func (e *errWriter) Write(buf []byte) (int, error) {if e.err != nil {return 0, e.err}var n intn, e.err = e.Writer.Write(buf)return n, nil}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}
上面的代码中,会使用 io.Writer 对请求的 resp 进行多次写入,但是我们不 用每次写入都进行一次错误处理,可以将每次写入返回的 err 封装在一个名为 errWriter 的结构体中,每次写入时使用这个结构体,调用结构体实现的 Write 方法写入时就会对历史写入返回的 err 进行处理。
Wrap Errors 封装错误
You should only handle errors once. Logging or just return it to the caller.
我们只需要进行一次错误处理,一层层封装返回给调用上层,或是直接本地处理后不再抛给上层。
需要注意,如果使用 fmt.Errorf 封装原始错误后抛给上层,会破坏原始的错误,导致上层等值判断失败。
打印日志的一些问题
如果日志与错误无关,而且对于调试找到错误根因没有任何帮助,那么这条日志不应该被打印。
打印日志是因为某些操作失败了,而日志包含了答案。
发生了错误就应该打印且只打印一次日志。
打印的日志需要包含错误的完整信息。
总结:
选择使用 wrap error 的包应该不具有较高的重用性,否则就可能有冗余的错误堆栈信息,具有最高重用性的包最好返回根错误值。
如果函数本身不打算处理错误,那么应该将足够的上下文 wrap 到 error 中返回给调用上层。
如果错误已经被处理了,那么错误应该就不再是错误,不应该再 return err 给上游调用者。
errors 包的几个使用技巧
在应用代码中,因为业务逻辑而导致的一些错误,使用 errors.New 或者 errors.Errorf 返回错误。
如果调用其他包内的函数,通常直接简单的返回原始 err。
如果调用的是第三方的库或是标准库,考虑使用 errors.Wrap 或者 errors.Wrapf 保存堆栈信息。
可以使用 errors.Cause 方法获取 wrap 之前的 root error,再与 sentinel errors 进行等值判定。
Go 2 Error Inspection
go1.13 errors 包中包含了两个用于检查错误的新函数:Is 和 As。
调用 errors.Is 底层会不断的调用 error 的 unwrap 方法尝试取到底层的错误类型。
errors.As 方法类似于使用断言将错误转为特定的类型。go1.13 支持使用 fmt.Errorf 结合 %w 谓词将底层错误包装入更多的上下文信息后返回,同时不影响 errors.Is 和 errors.As 对错误的判断。




