很多人吐槽Go语言错误处理太繁琐了,代码里面到处都是错误判断” if err != nil “。
Go语言错误是通过返回值,强迫调用者立即对错误进行处理。要么忽略,要么立即处理。相信大家在平时开发也好,在开源项目中也好都能看到 到处都是 if err != nil的判断。我们开发时要注意:不要只检查错误,要优雅地处理错误,我们可以通过消除错误来消除错误。接下来我们就来一起来优雅地处理错误。
- 通过消除错误来优雅地处理错误,避免到处是if err != nil
- Wrap Errors
- 如何优雅的Wrap Errors
- 如何优雅地处理日志
- 错误总结
1.通过消除错误来优雅地处理错误,避免到处是if err != nil
1.1.方法签名一致直接返回就好
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)
}
1.2.如果一个函数或方法太复杂,内部需要调用多个方法。可以提供一个Err()方法在最后调用Err()返回错误
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()
}
通过以上2个示例发现,每个方法调用就这样处理,就成了大家吐槽,Go语言到处都是if err != nil。小伙伴不信的话去看 Go 很多项目都是这样写,现在可能好点了,早起的Go项目大多数都写的。
2.Wrap Errors
为什么要使用 wrap error
1.底层抛出来了一个 error 你不知道是一个什么错误从哪里抛出来的,因为缺少了上下文。
2.有时候我们希望保留原始错误,又想携带更多的信息。
Wrap Error 是 Go 1.13中新增的特性,为了让我们优雅地处理业务逻辑中的错误。因为一些历史原因,go 1.13 wrap error不支持获取堆栈信息。后面我们一起使用”pkg/errors“和 go 1.13结合来处理错误。
2.1.如何生成Wrap Error
2.2. Is()方法
2.3. As() 方法
2.4. Unwrap() 方法
2.5. github.com/pkg/errors包与go 1.13 error结合使用
2.1.如何生成Wrap Error
fmt.Errorf():包装一个错误
// 包裹错误
err := errors.New("raw error")
errWrap1 := fmt.Errorf("wrap1 \n%w", err)
errWrap2 := fmt.Errorf("wrap2 \n%w", errWrap1)
fmt.Println(errWrap1)
fmt.Println()
fmt.Println(errWrap2)
/*输出:
wrap1
raw error
wrap2
wrap1
raw error
*/
2.2. Is()方法
当错误被 Wrap Error后我们上层没办法知道底层错误是什么,Is方法就是做这个事的。
Is()方法:检测 一个 error 是否包含指定的错误
// errors.Is(err, target error) bool
err := errors.New("raw error")
errWrap1 := fmt.Errorf("wrap1 \n%w", err)
errWrap2 := fmt.Errorf("wrap2 \n%w", errWrap1)
fmt.Println(errors.Is(errWrap1, err))// true
fmt.Println(errors.Is(errWrap2, errWrap1))// true
fmt.Println(errors.Is(errWrap2, errors.New("raw error")))// false
2.3. As() 方法
当我们使用 Wrap Error错误后,没办法直接使用断言去判断底层错误是啥。
As():把错误转换成指定的目标错误,类似于断言。
// errors.As(err error, target interface{}) bool
err := errors.New("raw error")
errWrap1 := fmt.Errorf("wrap1 \n%w", err)
errWrap2 := fmt.Errorf("wrap2 \n%w", errWrap1)
errWrapAs := errors.New("raw error")
var perr *os.PathError
fmt.Println(errors.As(errWrap2, &errWrapAs))// true
fmt.Println(errors.As(errWrap2, &perr))// false
2.4. Unwrap() 方法
Unwrap() :返回外层的error
// errors.Unwrap(err error) error
err := errors.New("raw error")
errWrap1 := fmt.Errorf("wrap1 \n%w", err)
errWrap2 := fmt.Errorf("wrap2 \n%w", errWrap1)
fmt.Println(errors.Unwrap(errWrap2))
fmt.Println()
fmt.Println(errors.Unwrap(errors.Unwrap(errWrap2)))
/*
wrap1
raw error
raw error
*/
2.5. github.com/pkg/errors包与go 1.13 error结合使用
”pkg/errors“对go 1.13 error做了兼容,所以我们可以直接使用”pkg/errors“
pkg/errors.New():创建包含堆栈信息的错误
pkg/errors.Wrap():打包一个包含堆栈信息的错误
pkg/errors.WithMessage():使用Message包装错误错误,不包含堆栈信息
package main
import (
"fmt"
pkgErrors "github.com/pkg/errors"
)
func main() {
err := pkgErrors.New("raw error")// 创建错误会包含堆栈信息
fmt.Printf("%+v", err)
}
/**输出:
raw error
main.main
/Users/zhaoweijie/work/develop/go/hello/main.go:10
runtime.main
/usr/local/Cellar/go/1.16/libexec/src/runtime/proc.go:225
runtime.goexit
/usr/local/Cellar/go/1.16/libexec/src/runtime/asm_amd64.s:1371%
*/
package main
import (
"errors"
"fmt"
pkgErrors "github.com/pkg/errors"
)
func main() {
err := errors.New("raw error")
wrapErr := pkgErrors.Wrap(err, "wrap 1") // pkg error wrap 打包错误会包含堆栈信息
fmt.Printf("%+v", wrapErr)
}
/**输出:
raw error
wrap 1
main.main
/Users/zhaoweijie/work/develop/go/hello/main.go:12
runtime.main
/usr/local/Cellar/go/1.16/libexec/src/runtime/proc.go:225
runtime.goexit
/usr/local/Cellar/go/1.16/libexec/src/runtime/asm_amd64.s:1371
*/
package main
import (
"errors"
"fmt"
pkgErrors "github.com/pkg/errors"
)
func main() {
err := errors.New("raw error")
wrapErr := pkgErrors.WithMessage(err, "wrap 1") // 不会包含堆栈信息
fmt.Printf("%+v", wrapErr)
}
/**输出:
raw error
wrap 1
*/
3.如何优雅的Wrap Errors
为了避免多次wrap带来的性能影响和日志变得司空见惯,导致日志中有很多不必要的日志产生的噪音。
我们应该遵守以下wrap error的使用规则
3.1.在标准库、第三方库、基础库、kit库产生的错误应该直接返回,而不应该wrap。
3.2. 在应用层代码中如果你调用了标准库、第三方和基础库产生的错误,你应该使用pkg errors.wrap()返回错误
3.3.如果你调用了本包的其他函数,你不应该wrap
3.4.在应用层代码中,如果你判断某个业务逻辑产生了一个错误,你应该使用 pkg errors.New()返回错误
3.1.在标准库、第三方库、基础库、kit库产生的错误应该直接返回,而不应该wrap。
如果在标准库、第三方库、基础库、kit库中如果你wrap,调用者不知道有没有wrap,再去wrap了,如果该包被其他包引入,导致被wrap多次。
3.2. 在应用层代码中如果你调用了标准库、第三方和基础库产生的错误,你应该使用pkg errors.wrap()返回错误
import (
pkgErrors "github.com/pkg/errors"
)
func Service() error {
_, err := http.Get("#34;)
return pkgErrors.Wrap(err, "http.get error")
}
3.2.如果你调用了本包内的其他函数,你不应该wrap
package service
import (
"github.com/pkg/errors"
)
func service() error {
err := serviceDemoFun()
// 调用了本包(service)的其他函数,你不应该wrap
if err != nil {
return err
}
// 业务代码
return nil
}
func serviceDemoFun() error {
return errors.New("service demo fun raw error")
}
3.3.在应用层代码中,如果你判断某个业务逻辑产生了一个错误,你应该使用 pkg errors.New()返回错误
import (
"github.com/pkg/errors"
)
func service() error {
str := ""
if str == "" {
return errors.New("serviceDemoFun result value is empty")
}
return nil
}
4.如何优雅地处理日志
4.1.直接返回错误,而不是在每个产生错误的地方记录日志
4.2.错误应包含足够并且有效的上下文信息
4.3.在应用程序的顶部记录日志
4.4.在goroutine的入口处记录日志
4.1.直接返回错误,而不是在每个产生错误的地方记录日志
package main
import (
"fmt"
"log"
"github.com/pkg/errors"
)
func main() {
wrapErr := service()
fmt.Printf("%+v", wrapErr)
}
// 应用层服务
func service() error {
err := domain()
if err != nil {
log.Printf("%s %v", "service error", err)
}
// 业务代码
return err
}
// 业务逻辑层
func domain() error {
err := dao()
if err != nil {
log.Printf("%s %v", "domain error", err)
}
// 业务代码
return err
}
// 数据层
func dao() error {
err := errors.New("dao raw error")
log.Printf("%v", "dao raw error")
return err
}
/*
2021/08/27 01:18:35 dao raw error
2021/08/27 01:18:35 domain error dao raw error
2021/08/27 01:18:35 service error dao raw error
dao raw error
main.dao
/Users/zhaoweijie/work/develop/go/hello/main.go:38
main.domain
/Users/zhaoweijie/work/develop/go/hello/main.go:27
main.service
/Users/zhaoweijie/work/develop/go/hello/main.go:17
main.main
/Users/zhaoweijie/work/develop/go/hello/main.go:11
runtime.main
/usr/local/Cellar/go/1.16/libexec/src/runtime/proc.go:225
runtime.goexit
/usr/local/Cellar/go/1.16/libexec/src/runtime/asm_amd64.s:1371%
*/
修改后
package main
import (
"log"
"github.com/pkg/errors"
)
func main() {
wrapErr := service()
log.Printf("%s %v", "service error", wrapErr)
}
// 应用层服务
func service() error {
err := domain()
// 业务代码
return err
}
// 业务逻辑层
func domain() error {
err := dao()
// 业务代码
return err
}
// 数据层
func dao() error {
err := errors.New("dao raw error")
return err
}
4.2.只记录错误必要的上下文信息
1. 比如不应该记录http接口的响应结果,因为99%以上的错误都可以通过请求参数找到错误原因
2.比如一个方法有5个参数,但是与错误有关的只有2个参数。这时候日志应该记录与错误有关的2个错误参数
4.3.在应用程序的顶部记录日志
package main
import (
"log"
"github.com/pkg/errors"
)
func main() {
// 在顶层处理错误
wrapErr := service()
log.Printf("%s %v", "service error", wrapErr)
}
// 应用层服务
func service() error {
return domain()
}
// 业务逻辑层
func domain() error {
return dao()
}
// 数据层
func dao() error {
return errors.New("dao raw error")
}
4.4.在goroutine的入口处记录日志
package main
import (
"errors"
"log"
)
func main() {
Go(func() error {
panic("goroutine1 panic error")
})
Go(func() error {
return errors.New("goroutine2 error")
})
for {
}
}
func Go(x func() error) {
go func() {
var err error
// 在goroutine的入库出记录日志
defer func() {
if errFatal := recover(); errFatal != nil {
log.Printf("%+v", errFatal)
}
if err != nil {
log.Printf("%+v", err)
}
}()
err = x()
}()
}
/**输出:
2021/08/27 01:42:31 goroutine2 error
2021/08/27 01:42:31 goroutine1 panic error
*/
5.错误总结
1. 优先处理失败,而不是成功
2. 完全控制代码中的错误,做你想做的任何事
3. 将错误作为一等公民,强迫调用者立即处理错误
4. 没有隐藏的控制流
5. 错误应该只处理一次(记录日志也算,wrap错误也算)
6. 如果你吞掉了一个错误,你应该保证程序100%的完整性
7. 第三方库、基础库、kit库中产生的错误不应该wrap
8. 在应用层中调用第三方库产生的产生了一个错误应该wrap
9. 在应用层中判断业务逻辑产生了一个错误,你应该wrap
10. 如果你调用了本包的其他函数,你不应该wrap
11. 直接返回错误,而不是在每个产生错误的地方记录日志
12. 错误应该只记录必要的上下文
13. 在应用程序的顶部记录日志
14. 在goroutine的入口处记录日志
对Go语言错误和日志处理,你有其他建议或者不同的意见欢迎在下方留言。