Go 的设计者认为并非所有的异常都是例外,也不是所有的错误都要使程序崩溃。只要你能从错误中正常恢复,你就应该恢复。这样程序才具有鲁棒性。Go需要程序员为了 error,花费费额外的精力,这也会使得程序员更能将软件写的更加健壮、更加稳定。
如何处理错误
在实际项目开发、运维过程中,会经常碰到如下问题:
- 函数该如何返回错误,是用值,还是用特殊的错误类型
- 如何检查被调用函数返回的错误,是判断错误值,还是用类型断言
- 程序中每层代码在碰到错误的时候,是每层都处理,还是只用在最上层处理,如何做到优雅
- 日志中的异常信息不够完整、缺少 stack strace,不方便定位错误原因
Go语言中三种错误处理策略
官方在 2011 年曾发布过一篇文章教大家如何处理 error。总结起来范式有三种:
- 返回和检查错误值:通过特定值表示成功和不同的错误,上层代码检查错误的值,来判断被调用 func 的执行状态。
- 自定义错误类型:通过自定义的错误类型来表示特定的错误,上层代码通过类型断言判断错误的类型。
- 隐藏内部细节的错误处理:假设上层代码不知道被调用函数返回的错误任何细节,直接再向上返回错误。
1. 返回和检查错误值
用 errors.New(str string)
定义错误常量, 让调用方去判断返回的 err 是否等于这个常量, 来进行区分处理。这种策略是最不灵活的错误处理策略,上层代码需要判断返回错误值是否等于特定值。如果想修改返回的错误值,则会破坏上层调用代码的逻辑。
高内聚、低耦合 是衡量公共库质量的一个重要方面,而返回特定错误值的方式,增加了公共库和调用代码的耦合性。让模块之间产生了依赖。
2. 自定义错误类型
自定义 struct type
实现 error 接口, 调用方用类型断言转成特定的 struct type, 拿到更结构化的错误信息。这种方式相比于 返回和检查错误值,很大一个优点在于可以将 底层错误 包起来一起返回给上层,这样可以提供更多的上下文信息。然而,这种方式依然会增加模块之间的依赖。
3. 隐藏内部细节的错误处理
这种策略之所以叫 隐藏内部细节的错误处理,是因为当上层代码碰到错误发生的时候,不知道错误的内部细节。作为上层代码,你需要知道的就是被调用函数是否正常工作。如果你接受这个原则,将极大降低模块之间的耦合性。
最合适的错误处理策略
很明显第三种策略耦合性最低。然而,第三种方式也存在一些问题:
- 没有详细错误信息,比如 stack trace 帮助定位错误原因
- 如何优雅的处理错误
- 有些场景需要了解错误细节,比如网络调用,需要知道是否是瞬时的中断
- 是否每层捕捉到错误的时候都需要处理
func AuthenticateRequest(r *Request) error {
err := authenticate(r.User)
if err != nil {
return fmt.Errorf("authenticate failed: %v", err) // authenticate failed: No such file or directory
}
return nil
}
这里用 fmt.Errorf(fmt string, args... interface{})
增加一些上下文信息, 用文字的方式告诉调用方哪里出错了, 让调用方打错误日志出来。
处关于 error 的 「箴言」
为了行为断言错误,而非为了类型 Assert errors for behaviour, not type
在有些场景下,仅仅知道是否出错是不够的。比如,和进程外其它服务通信,需要了解错误的属性,以决定是否需要重试操作。这种情况下,不要判断错误值或者错误的类型,我们可以判断错误是否实现某个行为。
type temporary interface {
Temporary() bool // IsTemporary returns true if err is temporary.
}
func IsTemporary(err error) bool {
te, ok := err.(temporary)
return ok && te.Temporary()
}
这种实现方式的好处在于,不需要知道具体的错误类型,也就不需要引用定义了错误类型的三方 package。如果你是底层代码的开发者,哪天你想更换一个实现更好的 error,也不用担心影响上层代码逻辑。如果你是上层代码的开发者,你只需要关注 error 是否实现了特定行为,不用担心引用的三方 package 升级后,程序逻辑失败。
不要忽略错误,也不要重复处理错误 Don’t just check errors, handle them gracefully
遇到错误,而不去处理,导致信息缺失,会增加后期的运维成本。而重复处理,添加了不必要的处理逻辑,导致信息冗余,也会增加后期的运维成本。
func Write(w io.Writer, buf []byte) error {
_, err := w.Write(buf)
if err != nil {
log.Println("unable to write:", err) // 第1次错误处理
return err
}
return nil
}
func main() {
// create writer and read data into buf
err := Write(w, buf)
if err != nil {
log.Println("Write error:", err) // 第2次错误处理
os.Exit(1)
}
os.Exit(0)
}
重构代码,减少因为 error 判断而带来的冗余 Only handle errors once
func myHandler(w http.Response, r *http.Request) {
err := validateRequest(r)
if err != nil {
log.Printf("error validating request to myHandler - err: %v", err)
w.WriteHeader(http.StatusInternalServerError)
return
}
user, err := getUserFromRequest(r)
if err != nil {
log.Printf("error getting user from request in myHandler - err: %v", err)
w.WriteHeader(http.StatusInternalServerError)
return
}
dataset, err := db.GetUserData(user)
if err != nil {
log.Printf("error retrieving user data in myHandler - err: %v", err)
w.WriteHeader(http.StatusInternalServerError)
return
}
buffer := newBuffer()
err := serialize.UserData(dataset, &buffer)
if err != nil {
log.Printf("error serializing user data in myHandler - err %v", err)
w.WriteHeader(http.StatusInternalServerError)
return
}
err := buffer.WriteTo(w);
if err != nil {
log.Printf("error writing buffer to response in myHandler - err %v", err)
return
}
}
func myHandler(w http.Response, r *http.Request) {
var err error
defer func() {
if err != nil {
log.Printf("error in myHandler - error: %v", err)
w.WriteHeader(http.StatusInternalServerErrror)
}
}()
err = validateRequest(r)
if err != nil { return }
user, err := getUserFromRequest(r)
if err != nil { return }
dataset, err := db.GetUserData(user)
if err != nil { return }
buffer := newBuffer()
err = serialize.UserData(dataset, &buffer)
if err != nil { return }
err2 := buffer.WriteTo(w)
if err2 != nil {
log.Printf("error writing buffer to response in myHandler - error %v", err2)
return
}
}
结论
读一下Can new Go errors wrapper replace pkg/errors? 和 Golang Error Handling — Best Practice in 2020,还是使用 pkg/errors
,因为:
- go 1.13 的 error 并不会携带调用栈,只有出错信息。
pkg/errors
包的 New, Errorf, Wrap, Wrapf
方法会将调用栈封装上,然后在 %+v
的时候打印出来。
import (
"fmt"
"os"
"github.com/pkg/errors"
)
func main() {
f, err := os.Open("notes.txt")
if err != nil {
err = errors.Wrap(err, "Error opening file")
fmt.Printf(" %+v", err)
}
defer f.Close()
}
错误判定的例子
package main
import (
"fmt"
"github.com/pkg/errors"
)
type fileError struct{}
// error 类型是一个接口类型,只要实现了 Error() 方法,就是实现了 error
func (fe *fileError) Error() string {
return "文件错误"
}
type temporaryError interface {
Temporary() bool
}
func (fe *fileError) Temporary() bool {
return true
}
// IsTemporary returns true if err is temporaryError.
func IsTemporary(err error) bool {
// 在无论何时需要检查错误与特定值或类型相匹配时,都应先用 Cause 功能恢复原始错误
te, ok := errors.Cause(err).(temporaryError)
return ok && te.Temporary()
}
func main() {
err := &fileError{}
fmt.Println(IsTemporary(err))
}
判断之前先使用 Cause
取出错误,做断言,最后,递归地调用 Temporary
函数。如果错误没实现 temporaryError
接口,就会断言失败,返回 false。
比如:拿到网络请求返回的 error 后,调用 IsTemporary
函数,如果返回 true,那就重试。这么做的好处是在进行网络请求的包里,不需要 import 引用定义错误的包。
它的使用非常简单,如果我们要新生成一个错误,可以使用 New 函数,其生成的错误自带调用堆栈信息。
func New(message string) error
如果有一个现成的 error,我们需要对他进行再次包装处理,这时候有三个函数可以选择。
// 只附加新的信息
func WithMessage(err error, message string) error
// 只附加调用堆栈信息
func WithStack(err error) error
// 同时附加堆栈和信息 Wrap annotates cause with a message.
func Wrap(err error, message string) error
// 获得最根本的错误原因 Cause unwraps an annotated error.
func Cause(err error) error
goErrorHandlingSample 这个 repo 中的例子演示了,不同错误处理方式,输出的错误信息的区别。
以上的错误我们都包装好了,也收集好了,那么怎么把他们里面存储的堆栈、错误原因等这些信息打印出来呢?其实,这个错误处理库的错误类型,都实现了 Formatter 接口,我们可以通过 fmt.Printf
函数输出对应的错误信息。
%s,%v //功能一样,输出错误信息,不包含堆栈
%q //输出的错误信息带引号,不包含堆栈
%+v //输出错误信息和堆栈
以上如果有循环包装错误类型的话,会递归的把这些错误都会输出。
通过使用这个错误库,我们可以收集更多的信息,可以让我们更容易的定位问题。收集的这些信息不止可以输出到控制台,也可以当做日志,使用输出到相应的 Log 日志里,便于分析问题。
func main() {
err := a.A()
if err != nil {
fmt.Println(PrintMessage(err)) // 打印普通信息
fmt.Println(PrintStack(err)) // 打印信息附带堆栈信息
return
}
fmt.Println("ok")
}
// 打印普通信息,没有堆栈信息
func PrintMessage(err error) string {
return err.Error()
}
// 打印详细信息,附带堆栈信息。
func PrintStack(err error) string {
errMsg := fmt.Sprintf("%+v", err)
return CleanPath(errMsg)
}
// 脱敏
func CleanPath(s string) string {
return strings.ReplaceAll(s, GetCurrentPath() + "/", "")
}
// 获取当前项目目录
func GetCurrentPath() string {
getwd, err := os.Getwd()
if err != nil {
return ""
}
return strings.Replace(getwd, "\\", "/", -1)
}
扩展
它也可以兼容新的 errors,详见 https://github.com/pkg/errors/pull/206/files。
只要实现了 Unwrap 方法即可。
相关链接