最佳实践之Golang错误处理 1、原生错误处理 2、开源e

1、原生错误处理

Go 语言通过内置的错误接口提供了非常简单的错误处理机制。
error类型是一个接口类型,这是它的定义:

1
2
3
go复制代码type error interface {
    Error() string
}

我们可以在编码中通过实现 error 接口类型来生成错误信息。
函数通常在最后的返回值中返回错误信息。使用errors.New 可返回一个错误信息:

1
2
3
4
5
6
go复制代码func Sqrt(f float64) (float64, error) {
if f < 0 {
return 0, errors.New("math: square root of negative number")
}
// 实现
}

在下面的例子中,我们在调用Sqrt的时候传递的一个负数,然后就得到了non-nil的error对象,将此对象与nil比较,结果为true,所以fmt.Println(fmt包在处理error时会调用Error方法)被调用,以输出错误,请看下面调用的示例代码:

1
2
3
4
go复制代码result, err:= Sqrt(-1)
if err != nil {
fmt.Println(err)
}

2、开源error包

github.com/pkg/errors包在原生error包基础上增加了以下常用的功能:

  • 可以打印error的堆栈信息:打印错误需要%+v才能详细输出
  • 使用Wrap或Wrapf,初始化一个error
  • 使用errors.WithMessage可以在原来的error基础上再包装一层,包含原有error信息
  • errors.Is,用于判断error类型,可根据error类型不同做不同处理
  • errors.As,用于解析error

具体使用案例见全局错误处理一节。

3、工程中错误处理

3.1 需求整理

  • 自定义error信息,并进行编码整理
    • controller层可以判断自定义error类型,最终判断是按info处理,还是按error处理
  • 可以打印error初始发生的位置(获取error的调用栈)
  • 确认当前系统定位:
    • 用户,获取TagMessage
    • 上游服务,需要错误码映射
    • 日志监控、监控TagMessage

下面在一个工程化的项目中利用github.com/pkg/errors包,完整实现一套的错误处理机制

3.2 方式一:Map保存错误码与Message的映射

3.2.1 定义错误信息

新建error_handler.go

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
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
go复制代码package error_handle

import (
"github.com/pkg/errors"
)

// 1、自定义error结构体,并重写Error()方法
// 错误时返回自定义结构
type CustomError struct {
Code int `json:"code"` // 业务码
TagMessage string `json:"message"` // 描述信息
}

func (e *CustomError) Error() string {
return e.TagMessage
}

// 2、定义errorCode
const (
// 服务级错误码
ServerError = 10101
TooManyRequests = 10102
ParamBindError = 10103
AuthorizationError = 10104
CallHTTPError = 10105
ResubmitError = 10106
ResubmitMsg = 10107
HashIdsDecodeError = 10108
SignatureError = 10109

// 业务模块级错误码
// 用户模块
IllegalUserName = 20101
UserCreateError = 20102
UserUpdateError = 20103
UserSearchError = 20104

// 授权调用方
AuthorizedCreateError = 20201
AuthorizedListError = 20202
AuthorizedDeleteError = 20203
AuthorizedUpdateError = 20204
AuthorizedDetailError = 20205
AuthorizedCreateAPIError = 20206
AuthorizedListAPIError = 20207
AuthorizedDeleteAPIError = 20208

// 管理员
AdminCreateError = 20301
AdminListError = 20302
AdminDeleteError = 20303
AdminUpdateError = 20304
AdminResetPasswordError = 20305
AdminLoginError = 20306
AdminLogOutError = 20307
AdminModifyPasswordError = 20308
AdminModifyPersonalInfoError = 20309

// 配置
ConfigEmailError = 20401
ConfigSaveError = 20402
ConfigRedisConnectError = 20403
ConfigMySQLConnectError = 20404
ConfigMySQLInstallError = 20405
ConfigGoVersionError = 20406

// 实用工具箱
SearchRedisError = 20501
ClearRedisError = 20502
SearchRedisEmpty = 20503
SearchMySQLError = 20504

// 菜单栏
MenuCreateError = 20601
MenuUpdateError = 20602
MenuListError = 20603
MenuDeleteError = 20604
MenuDetailError = 20605

// 借书
BookNotFoundError = 20701
BookHasBeenBorrowedError = 20702
)

// 3、定义errorCode对应的文本信息
var codeTag = map[int]string{
ServerError: "Internal Server Error",
TooManyRequests: "Too Many Requests",
ParamBindError: "参数信息有误",
AuthorizationError: "签名信息有误",
CallHTTPError: "调用第三方 HTTP 接口失败",
ResubmitError: "Resubmit Error",
ResubmitMsg: "请勿重复提交",
HashIdsDecodeError: "ID参数有误",
SignatureError: "SignatureError",

IllegalUserName: "非法用户名",
UserCreateError: "创建用户失败",
UserUpdateError: "更新用户失败",
UserSearchError: "查询用户失败",

AuthorizedCreateError: "创建调用方失败",
AuthorizedListError: "获取调用方列表页失败",
AuthorizedDeleteError: "删除调用方失败",
AuthorizedUpdateError: "更新调用方失败",
AuthorizedDetailError: "获取调用方详情失败",
AuthorizedCreateAPIError: "创建调用方API地址失败",
AuthorizedListAPIError: "获取调用方API地址列表失败",
AuthorizedDeleteAPIError: "删除调用方API地址失败",

AdminCreateError: "创建管理员失败",
AdminListError: "获取管理员列表页失败",
AdminDeleteError: "删除管理员失败",
AdminUpdateError: "更新管理员失败",
AdminResetPasswordError: "重置密码失败",
AdminLoginError: "登录失败",
AdminLogOutError: "退出失败",
AdminModifyPasswordError: "修改密码失败",
AdminModifyPersonalInfoError: "修改个人信息失败",

ConfigEmailError: "修改邮箱配置失败",
ConfigSaveError: "写入配置文件失败",
ConfigRedisConnectError: "Redis连接失败",
ConfigMySQLConnectError: "MySQL连接失败",
ConfigMySQLInstallError: "MySQL初始化数据失败",
ConfigGoVersionError: "GoVersion不满足要求",

SearchRedisError: "查询RedisKey失败",
ClearRedisError: "清空RedisKey失败",
SearchRedisEmpty: "查询的RedisKey不存在",
SearchMySQLError: "查询mysql失败",

MenuCreateError: "创建菜单失败",
MenuUpdateError: "更新菜单失败",
MenuDeleteError: "删除菜单失败",
MenuListError: "获取菜单列表页失败",
MenuDetailError: "获取菜单详情失败",

BookNotFoundError: "书未找到",
BookHasBeenBorrowedError: "书已经被借走了",
}

func Text(code int) string {
return codeTag[code]
}

// 4、新建自定义error实例化
func NewCustomError(code int) error {
// 初次调用得用Wrap方法,进行实例化
return errors.Wrap(&CustomError{
Code: code,
TagMessage: codeTag[code],
}, "")
}

3.3 自定义Error使用

新建测试文件:error_handler_test.go

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
go复制代码package error_handle

import (
"fmt"
"github.com/pkg/errors"
"testing"
)

func TestText(t *testing.T) {
books := []string{
"Book1",
"Book222222",
"Book3333333333",
}

for _, bookName := range books {
err := searchBook(bookName)

// 特殊业务场景:如果发现书被借走了,下次再来就行了,不需要作为错误处理
if err != nil {
// 提取error这个interface底层的错误码,一般在API的返回前才提取
// As - 获取错误的具体实现
var myError = new(CustomError)
// As - 解析错误内容
if errors.As(err, &myError) {
fmt.Printf("AS中的信息:当前书为: %s ,error code is %d, message is %s\n", bookName, myError.Code, myError.TagMessage)
}

// 特殊场景,指定错误(ErrorBookHasBeenBorrowed)时,打印即可,不返回错误
// Is - 判断错误是否为指定类型
if errors.Is(err, NewCustomError(BookHasBeenBorrowedError)) {
fmt.Printf("IS中的信息:%s 已经被借走了, 只需按Info处理!\n", bookName)
err = nil
}else {
// 如果已有堆栈信息,应调用WithMessage方法
newErr := errors.WithMessage(err, "WithMessage err")
fmt.Printf("IS中的信息:%s 未找到,应该按Error处理! ,newErr is %s\n", bookName , newErr)
}
}
}
}

func searchBook(bookName string) error {
// 1 发现图书馆不存在这本书 - 认为是错误,需要打印详细的错误信息
if len(bookName) > 10 {
return NewCustomError(BookHasBeenBorrowedError)
} else if len(bookName) > 6 {
// 2 发现书被借走了 - 打印一下被接走的提示即可,不认为是错误
return NewCustomError(BookHasBeenBorrowedError)
}
// 3 找到书 - 不需要任何处理
return nil
}

3.3 方式二:借助generate简化代码(建议使用)

方式一维护错误码与错误信息的关系较为复杂,我们可以借助go generate来自动生成代码。

3.3.1 安装stringer

stringer不是Go自带工具,需要手动安装。执行如下命令即可

1
go复制代码go get golang.org/x/tools/cmd/stringer

3.3.1 定义错误信息

新建error_handler.go。在error_handler中,增加注释//go:generate stringer -type ErrCode -linecomment。执行go generate,会生成新的文件

image.png

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
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
go复制代码package error_handle

import (
"github.com/pkg/errors"
)

// 1、自定义error结构体,并重写Error()方法
// 错误时返回自定义结构
type CustomError struct {
Code ErrCode `json:"code"` // 业务码
Message string `json:"message"` // 业务码
}

func (e *CustomError) Error() string {
return e.Code.String()
}

type ErrCode int64 //错误码

// 2、定义errorCode
//go:generate stringer -type ErrCode -linecomment
const (
// 服务级错误码
ServerError ErrCode = 10101 // Internal Server Error
TooManyRequests ErrCode = 10102 // Too Many Requests
ParamBindError ErrCode = 10103 // 参数信息有误
AuthorizationError ErrCode = 10104 // 签名信息有误
CallHTTPError ErrCode = 10105 // 调用第三方HTTP接口失败
ResubmitError ErrCode = 10106 // ResubmitError
ResubmitMsg ErrCode = 10107 // 请勿重复提交
HashIdsDecodeError ErrCode = 10108 // ID参数有误
SignatureError ErrCode = 10109 // SignatureError

// 业务模块级错误码
// 用户模块
IllegalUserName ErrCode = 20101 // 非法用户名
UserCreateError ErrCode = 20102 // 创建用户失败
UserUpdateError ErrCode = 20103 // 更新用户失败
UserSearchError ErrCode = 20104 // 查询用户失败

// 配置
ConfigEmailError ErrCode = 20401 // 修改邮箱配置失败
ConfigSaveError ErrCode = 20402 // 写入配置文件失败
ConfigRedisConnectError ErrCode = 20403 // Redis连接失败
ConfigMySQLConnectError ErrCode = 20404 // MySQL连接失败
ConfigMySQLInstallError ErrCode = 20405 // MySQL初始化数据失败
ConfigGoVersionError ErrCode = 20406 // GoVersion不满足要求

// 实用工具箱
SearchRedisError ErrCode = 20501 // 查询RedisKey失败
ClearRedisError ErrCode = 20502 // 清空RedisKey失败
SearchRedisEmpty ErrCode = 20503 // 查询的RedisKey不存在
SearchMySQLError ErrCode = 20504 // 查询mysql失败

// 菜单栏
MenuCreateError ErrCode = 20601 // 创建菜单失败
MenuUpdateError ErrCode = 20602 // 更新菜单失败
MenuListError ErrCode = 20603 // 删除菜单失败
MenuDeleteError ErrCode = 20604 // 获取菜单列表页失败
MenuDetailError ErrCode = 20605 // 获取菜单详情失败

// 借书
BookNotFoundError ErrCode = 20701 // 书未找到
BookHasBeenBorrowedError ErrCode = 20702 // 书已经被借走了
)

// 4、新建自定义error实例化
func NewCustomError(code ErrCode) error {
// 初次调用得用Wrap方法,进行实例化
return errors.Wrap(&CustomError{
Code: code,
Message: code.String(),
}, "")
}

3.3.2 自定义Error使用

新建测试文件:error_handler_test.go

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
go复制代码package error_handle

import (
"fmt"
"github.com/pkg/errors"
"testing"
)

func TestText(t *testing.T) {
books := []string{
"Book1",
"Book222222",
"Book3333333333",
}

for _, bookName := range books {
err := searchBook(bookName)

// 特殊业务场景:如果发现书被借走了,下次再来就行了,不需要作为错误处理
if err != nil {
// 提取error这个interface底层的错误码,一般在API的返回前才提取
// As - 获取错误的具体实现
var customErr = new(CustomError)
// As - 解析错误内容
if errors.As(err, &customErr) {
//fmt.Printf("AS中的信息:当前书为: %s ,error code is %d, message is %s\n", bookName, customErr.Code, customErr.Message)
if customErr.Code == BookHasBeenBorrowedError {
fmt.Printf("IS中的info信息:%s 已经被借走了, 只需按Info处理!\n", bookName)
} else {
// 如果已有堆栈信息,应调用WithMessage方法
newErr := errors.WithMessage(err, "WithMessage err1")
// 使用%+v可以打印完整的堆栈信息
fmt.Printf("IS中的error信息:%s 未找到,应该按Error处理! ,newErr is: %+v\n", bookName, newErr)
}
}
}
}
}

func searchBook(bookName string) error {
// 1 发现图书馆不存在这本书 - 认为是错误,需要打印详细的错误信息
if len(bookName) > 10 {
return NewCustomError(BookNotFoundError)
} else if len(bookName) > 6 {
// 2 发现书被借走了 - 打印一下被接走的提示即可,不认为是错误
return NewCustomError(BookHasBeenBorrowedError)
}
// 3 找到书 - 不需要任何处理
return nil
}

4 总结

  1. CustomError 作为全局 error 的底层实现,保存具体的错误码和错误信息;
  2. CustomError向上返回错误时,第一次先用Wrap初始化堆栈,后续用WithMessage增加堆栈信息;
  3. error中解析具体错误时,用errors.As提取出CustomError,其中的错误码和错误信息可以传入到具体的API接口中;
  4. 要判断error是否为指定的错误时,用errors.Is + Handler Error的方法,处理一些特定情况下的逻辑;

Tips:

  1. 不要一直用errors.Wrap来反复包装错误,堆栈信息会爆炸,具体情况可自行测试了解
  2. 利用go generate可以大量简化初始化Erro重复的工作
  3. github.com/pkg/errors和标准库的error完全兼容,可以先替换、后续改造历史遗留的代码
  4. 一定要注意打印error的堆栈需要用%+v,而原来的%v依旧为普通字符串方法;同时也要注意日志采集工具是否支持多行匹配

我是简凡,一个励志用最简单的语言,描述最复杂问题的新时代农民工。求点赞,求关注,如果你对此篇文章有什么疑惑,欢迎在我的微信公众号中留言,我还可以为你提供以下帮助:

  • 帮助建立自己的知识体系
  • 互联网真实高并发场景实战讲解
  • 不定期分享Golang、Java相关业内的经典场景实践

我的博客:besthpt.github.io/

微信公众号:”简凡丶”

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

0%