在上一篇,我分享了对 官方Proposal 的一些见解,偏向于理论层面。
本篇里,我会具体到代码层面,谈谈如何在一个工程化的项目中利用github.com/pkg/errors
包,完整实现一套的错误处理机制。
// 全局的 错误号 类型,用于API调用之间传递
type MyErrorCode int
// 全局的 错误号 的具体定义
const (
ErrorBookNotFoundCode MyErrorCode = iota + 1
ErrorBookHasBeenBorrowedCode
)
// 内部的错误map,用来对应 错误号和错误信息
var errCodeMap = map[MyErrorCode]string{
ErrorBookNotFoundCode: "Book was not found",
ErrorBookHasBeenBorrowedCode: "Book has been borrowed",
}
// Sentinel Error: 即全局定义的Static错误变量
// 注意,这里的全局error是没有保存堆栈信息的,所以需要在初始调用处使用 errors.Wrap
var (
ErrorBookNotFound = NewMyError(ErrorBookNotFoundCode)
ErrorBookHasBeenBorrowed = NewMyError(ErrorBookHasBeenBorrowedCode)
)
func NewMyError(code MyErrorCode) *MyError {
return &MyError{
Code: code,
Message: errCodeMap[code],
}
}
// error的具体实现
type MyError struct {
// 对外使用 - 错误码
Code MyErrorCode
// 对外使用 - 错误信息
Message string
}
func (e *MyError) Error() string {
return e.Message
}
我们来模拟一个场景:
我去图书馆借几本书,会存在三个场景,分别的处理逻辑如下
func main() {
books := []string{
"Hamlet",
"Jane Eyre",
"War and Peace",
}
for _, bookName := range books {
fmt.Printf("%s start\n===\n", bookName)
err := borrowOne(bookName)
if err != nil {
fmt.Printf("%+v\n", err)
}
fmt.Printf("===\n%s end\n\n", bookName)
}
}
func borrowOne(bookName string) error {
// Step1: 找书
err := searchBook(bookName)
// Step2: 处理
// 特殊业务场景:如果发现书被借走了,下次再来就行了,不需要作为错误处理
if err != nil {
// 提取error这个interface底层的错误码,一般在API的返回前才提取
// As - 获取错误的具体实现
var myError = new(MyError)
if errors.As(err, &myError) {
fmt.Printf("error code is %d, message is %s\n", myError.Code, myError.Message)
}
// 特殊逻辑: 对应场景2,指定错误(ErrorBookHasBeenBorrowed)时,打印即可,不返回错误
// Is - 判断错误是否为指定类型
if errors.Is(err, ErrorBookHasBeenBorrowed) {
fmt.Printf("book %s has been borrowed, I will come back later!\n", bookName)
err = nil
}
}
return err
}
func searchBook(bookName string) error {
// 下面两个 error 都是不带堆栈信息的,所以初次调用得用Wrap方法
// 如果已有堆栈信息,应调用WithMessage方法
// 3 发现图书馆不存在这本书 - 认为是错误,需要打印详细的错误信息
if len(bookName) > 10 {
return errors.Wrapf(ErrorBookNotFound, "bookName is %s", bookName)
} else if len(bookName) > 8 {
// 2 发现书被借走了 - 打印一下被接走的提示即可,不认为是错误
return errors.Wrapf(ErrorBookHasBeenBorrowed, "bookName is %s", bookName)
}
// 1 找到书 - 不需要任何处理
return nil
}
Hamlet start
===
===
Hamlet end
没有任何错误信息
Jane Eyre start
===
error code is 2, message is Book has been borrowed
book Jane Eyre has been borrowed, I will come back later!
===
Jane Eyre end
打印被借走的提示,而错误被 err = nil
屏蔽。
War and Peace start
===
error code is 1, message is Book was not found
Book was not found
bookName is War and Peace
main.searchBook
/GoProject/godemo/main.go:98
main.borrowOne
/GoProject/godemo/main.go:71
main.main
/GoProject/godemo/main.go:60
runtime.main
/usr/local/go1.13.5/src/runtime/proc.go:203
runtime.goexit
/usr/local/go1.13.5/src/runtime/asm_amd64.s:1357
===
War and Peace end
打印了错误的详细堆栈,在IDE中调试非常方便,可以直接跳转到对应代码位置。
MyError
作为全局 error
的底层实现,保存具体的错误码和错误信息;MyError
向上返回错误时,第一次先用Wrap
初始化堆栈,后续用WithMessage
增加堆栈信息;error
中解析具体错误时,用errors.As
提取出MyError
,其中的错误码和错误信息可以传入到具体的API接口中;error
是否为指定的错误时,用errors.Is
+ Sentinel Error
的方法,处理一些特定情况下的逻辑;Tips:
github.com/pkg/errors
和标准库的error
完全兼容,可以先替换、后续改造历史遗留的代码error
的堆栈需要用%+v
,而原来的%v
依旧为普通字符串方法;同时也要注意日志采集工具是否支持多行匹配从现状来看,Go
语言的 Error Handling
已趋于共识,。
后续差异点就在底层 MyError
这块的实现,我个人认为会有如下三个方向:
Is
,As
等函数再进行一定的封装,使用起来更方便