1安聘、原生錯誤處理
Go 語言通過內(nèi)置的錯誤接口提供了非常簡單的錯誤處理機制榆俺。
error類型是一個接口類型检吆,這是它的定義:
type error interface {
Error() string
}
我們可以在編碼中通過實現(xiàn) error 接口類型來生成錯誤信息漠畜。
函數(shù)通常在最后的返回值中返回錯誤信息笼痹。使用errors.New 可返回一個錯誤信息:
func Sqrt(f float64) (float64, error) {
if f < 0 {
return 0, errors.New("math: square root of negative number")
}
// 實現(xiàn)
}
在下面的例子中,我們在調(diào)用Sqrt的時候傳遞的一個負數(shù)识补,然后就得到了non-nil的error對象族淮,將此對象與nil比較,結(jié)果為true凭涂,所以fmt.Println(fmt包在處理error時會調(diào)用Error方法)被調(diào)用祝辣,以輸出錯誤,請看下面調(diào)用的示例代碼:
result, err:= Sqrt(-1)
if err != nil {
fmt.Println(err)
}
2切油、開源error包
github.com/pkg/errors包在原生error包基礎(chǔ)上增加了以下常用的功能:
- 可以打印error的堆棧信息:打印錯誤需要%+v才能詳細輸出
- 使用Wrap或Wrapf蝙斜,初始化一個error
- 使用errors.WithMessage可以在原來的error基礎(chǔ)上再包裝一層,包含原有error信息
- errors.Is澎胡,用于判斷error類型孕荠,可根據(jù)error類型不同做不同處理
- errors.As,用于解析error
具體使用案例見全局錯誤處理一節(jié)攻谁。
3稚伍、工程中錯誤處理
3.1 需求整理
- 自定義error信息,并進行編碼整理
- controller層可以判斷自定義error類型戚宦,最終判斷是按info處理个曙,還是按error處理
- 可以打印error初始發(fā)生的位置(獲取error的調(diào)用棧)
- 確認當前系統(tǒng)定位:
- 用戶,獲取TagMessage
- 上游服務(wù)受楼,需要錯誤碼映射
- 日志監(jiān)控垦搬、監(jiān)控TagMessage
下面在一個工程化的項目中利用github.com/pkg/errors
包,完整實現(xiàn)一套的錯誤處理機制
3.2 方式一:Map保存錯誤碼與Message的映射
3.2.1 定義錯誤信息
新建error_handler.go
package error_handle
import (
"github.com/pkg/errors"
)
// 1艳汽、自定義error結(jié)構(gòu)體悼沿,并重寫Error()方法
// 錯誤時返回自定義結(jié)構(gòu)
type CustomError struct {
Code int `json:"code"` // 業(yè)務(wù)碼
TagMessage string `json:"message"` // 描述信息
}
func (e *CustomError) Error() string {
return e.TagMessage
}
// 2、定義errorCode
const (
// 服務(wù)級錯誤碼
ServerError = 10101
TooManyRequests = 10102
ParamBindError = 10103
AuthorizationError = 10104
CallHTTPError = 10105
ResubmitError = 10106
ResubmitMsg = 10107
HashIdsDecodeError = 10108
SignatureError = 10109
// 業(yè)務(wù)模塊級錯誤碼
// 用戶模塊
IllegalUserName = 20101
UserCreateError = 20102
UserUpdateError = 20103
UserSearchError = 20104
// 授權(quán)調(diào)用方
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對應(yīng)的文本信息
var codeTag = map[int]string{
ServerError: "Internal Server Error",
TooManyRequests: "Too Many Requests",
ParamBindError: "參數(shù)信息有誤",
AuthorizationError: "簽名信息有誤",
CallHTTPError: "調(diào)用第三方 HTTP 接口失敗",
ResubmitError: "Resubmit Error",
ResubmitMsg: "請勿重復(fù)提交",
HashIdsDecodeError: "ID參數(shù)有誤",
SignatureError: "SignatureError",
IllegalUserName: "非法用戶名",
UserCreateError: "創(chuàng)建用戶失敗",
UserUpdateError: "更新用戶失敗",
UserSearchError: "查詢用戶失敗",
AuthorizedCreateError: "創(chuàng)建調(diào)用方失敗",
AuthorizedListError: "獲取調(diào)用方列表頁失敗",
AuthorizedDeleteError: "刪除調(diào)用方失敗",
AuthorizedUpdateError: "更新調(diào)用方失敗",
AuthorizedDetailError: "獲取調(diào)用方詳情失敗",
AuthorizedCreateAPIError: "創(chuàng)建調(diào)用方API地址失敗",
AuthorizedListAPIError: "獲取調(diào)用方API地址列表失敗",
AuthorizedDeleteAPIError: "刪除調(diào)用方API地址失敗",
AdminCreateError: "創(chuàng)建管理員失敗",
AdminListError: "獲取管理員列表頁失敗",
AdminDeleteError: "刪除管理員失敗",
AdminUpdateError: "更新管理員失敗",
AdminResetPasswordError: "重置密碼失敗",
AdminLoginError: "登錄失敗",
AdminLogOutError: "退出失敗",
AdminModifyPasswordError: "修改密碼失敗",
AdminModifyPersonalInfoError: "修改個人信息失敗",
ConfigEmailError: "修改郵箱配置失敗",
ConfigSaveError: "寫入配置文件失敗",
ConfigRedisConnectError: "Redis連接失敗",
ConfigMySQLConnectError: "MySQL連接失敗",
ConfigMySQLInstallError: "MySQL初始化數(shù)據(jù)失敗",
ConfigGoVersionError: "GoVersion不滿足要求",
SearchRedisError: "查詢RedisKey失敗",
ClearRedisError: "清空RedisKey失敗",
SearchRedisEmpty: "查詢的RedisKey不存在",
SearchMySQLError: "查詢mysql失敗",
MenuCreateError: "創(chuàng)建菜單失敗",
MenuUpdateError: "更新菜單失敗",
MenuDeleteError: "刪除菜單失敗",
MenuListError: "獲取菜單列表頁失敗",
MenuDetailError: "獲取菜單詳情失敗",
BookNotFoundError: "書未找到",
BookHasBeenBorrowedError: "書已經(jīng)被借走了",
}
func Text(code int) string {
return codeTag[code]
}
// 4糟趾、新建自定義error實例化
func NewCustomError(code int) error {
// 初次調(diào)用得用Wrap方法,進行實例化
return errors.Wrap(&CustomError{
Code: code,
TagMessage: codeTag[code],
}, "")
}
3.3 自定義Error使用
新建測試文件:error_handler_test.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)
// 特殊業(yè)務(wù)場景:如果發(fā)現(xiàn)書被借走了甚牲,下次再來就行了义郑,不需要作為錯誤處理
if err != nil {
// 提取error這個interface底層的錯誤碼,一般在API的返回前才提取
// As - 獲取錯誤的具體實現(xiàn)
var myError = new(CustomError)
// As - 解析錯誤內(nèi)容
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 已經(jīng)被借走了, 只需按Info處理!\n", bookName)
err = nil
}else {
// 如果已有堆棧信息雏赦,應(yīng)調(diào)用WithMessage方法
newErr := errors.WithMessage(err, "WithMessage err")
fmt.Printf("IS中的信息:%s 未找到劫笙,應(yīng)該按Error處理! ,newErr is %s\n", bookName , newErr)
}
}
}
}
func searchBook(bookName string) error {
// 1 發(fā)現(xiàn)圖書館不存在這本書 - 認為是錯誤芙扎,需要打印詳細的錯誤信息
if len(bookName) > 10 {
return NewCustomError(BookHasBeenBorrowedError)
} else if len(bookName) > 6 {
// 2 發(fā)現(xiàn)書被借走了 - 打印一下被接走的提示即可,不認為是錯誤
return NewCustomError(BookHasBeenBorrowedError)
}
// 3 找到書 - 不需要任何處理
return nil
}
3.3 方式二:借助generate簡化代碼(建議使用)
方式一維護錯誤碼與錯誤信息的關(guān)系較為復(fù)雜填大,我們可以借助go generate來自動生成代碼戒洼。
3.3.1 安裝stringer
stringer不是Go自帶工具,需要手動安裝允华。執(zhí)行如下命令即可
go get golang.org/x/tools/cmd/stringer
3.3.1 定義錯誤信息
新建error_handler.go圈浇。在error_handler中,增加注釋//go:generate stringer -type ErrCode -linecomment靴寂。執(zhí)行g(shù)o generate磷蜀,會生成新的文件
package error_handle
import (
"github.com/pkg/errors"
)
// 1、自定義error結(jié)構(gòu)體百炬,并重寫Error()方法
// 錯誤時返回自定義結(jié)構(gòu)
type CustomError struct {
Code ErrCode `json:"code"` // 業(yè)務(wù)碼
Message string `json:"message"` // 業(yè)務(wù)碼
}
func (e *CustomError) Error() string {
return e.Code.String()
}
type ErrCode int64 //錯誤碼
// 2褐隆、定義errorCode
//go:generate stringer -type ErrCode -linecomment
const (
// 服務(wù)級錯誤碼
ServerError ErrCode = 10101 // Internal Server Error
TooManyRequests ErrCode = 10102 // Too Many Requests
ParamBindError ErrCode = 10103 // 參數(shù)信息有誤
AuthorizationError ErrCode = 10104 // 簽名信息有誤
CallHTTPError ErrCode = 10105 // 調(diào)用第三方HTTP接口失敗
ResubmitError ErrCode = 10106 // ResubmitError
ResubmitMsg ErrCode = 10107 // 請勿重復(fù)提交
HashIdsDecodeError ErrCode = 10108 // ID參數(shù)有誤
SignatureError ErrCode = 10109 // SignatureError
// 業(yè)務(wù)模塊級錯誤碼
// 用戶模塊
IllegalUserName ErrCode = 20101 // 非法用戶名
UserCreateError ErrCode = 20102 // 創(chuàng)建用戶失敗
UserUpdateError ErrCode = 20103 // 更新用戶失敗
UserSearchError ErrCode = 20104 // 查詢用戶失敗
// 配置
ConfigEmailError ErrCode = 20401 // 修改郵箱配置失敗
ConfigSaveError ErrCode = 20402 // 寫入配置文件失敗
ConfigRedisConnectError ErrCode = 20403 // Redis連接失敗
ConfigMySQLConnectError ErrCode = 20404 // MySQL連接失敗
ConfigMySQLInstallError ErrCode = 20405 // MySQL初始化數(shù)據(jù)失敗
ConfigGoVersionError ErrCode = 20406 // GoVersion不滿足要求
// 實用工具箱
SearchRedisError ErrCode = 20501 // 查詢RedisKey失敗
ClearRedisError ErrCode = 20502 // 清空RedisKey失敗
SearchRedisEmpty ErrCode = 20503 // 查詢的RedisKey不存在
SearchMySQLError ErrCode = 20504 // 查詢mysql失敗
// 菜單欄
MenuCreateError ErrCode = 20601 // 創(chuàng)建菜單失敗
MenuUpdateError ErrCode = 20602 // 更新菜單失敗
MenuListError ErrCode = 20603 // 刪除菜單失敗
MenuDeleteError ErrCode = 20604 // 獲取菜單列表頁失敗
MenuDetailError ErrCode = 20605 // 獲取菜單詳情失敗
// 借書
BookNotFoundError ErrCode = 20701 // 書未找到
BookHasBeenBorrowedError ErrCode = 20702 // 書已經(jīng)被借走了
)
// 4、新建自定義error實例化
func NewCustomError(code ErrCode) error {
// 初次調(diào)用得用Wrap方法剖踊,進行實例化
return errors.Wrap(&CustomError{
Code: code,
Message: code.String(),
}, "")
}
3.3.2 自定義Error使用
新建測試文件:error_handler_test.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)
// 特殊業(yè)務(wù)場景:如果發(fā)現(xiàn)書被借走了庶弃,下次再來就行了,不需要作為錯誤處理
if err != nil {
// 提取error這個interface底層的錯誤碼蜜宪,一般在API的返回前才提取
// As - 獲取錯誤的具體實現(xiàn)
var customErr = new(CustomError)
// As - 解析錯誤內(nèi)容
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 已經(jīng)被借走了, 只需按Info處理!\n", bookName)
} else {
// 如果已有堆棧信息,應(yīng)調(diào)用WithMessage方法
newErr := errors.WithMessage(err, "WithMessage err1")
// 使用%+v可以打印完整的堆棧信息
fmt.Printf("IS中的error信息:%s 未找到祥山,應(yīng)該按Error處理! ,newErr is: %+v\n", bookName, newErr)
}
}
}
}
}
func searchBook(bookName string) error {
// 1 發(fā)現(xiàn)圖書館不存在這本書 - 認為是錯誤圃验,需要打印詳細的錯誤信息
if len(bookName) > 10 {
return NewCustomError(BookNotFoundError)
} else if len(bookName) > 6 {
// 2 發(fā)現(xiàn)書被借走了 - 打印一下被接走的提示即可,不認為是錯誤
return NewCustomError(BookHasBeenBorrowedError)
}
// 3 找到書 - 不需要任何處理
return nil
}
4 總結(jié)
-
CustomError
作為全局error
的底層實現(xiàn)缝呕,保存具體的錯誤碼和錯誤信息澳窑; -
CustomError
向上返回錯誤時,第一次先用Wrap
初始化堆棧供常,后續(xù)用WithMessage
增加堆棧信息摊聋; - 從
error
中解析具體錯誤時,用errors.As
提取出CustomError
栈暇,其中的錯誤碼和錯誤信息可以傳入到具體的API接口中麻裁; - 要判斷
error
是否為指定的錯誤時,用errors.Is
+Handler Error
的方法源祈,處理一些特定情況下的邏輯煎源;
Tips:
- 不要一直用errors.Wrap來反復(fù)包裝錯誤,堆棧信息會爆炸香缺,具體情況可自行測試了解
- 利用go generate可以大量簡化初始化Erro重復(fù)的工作
github.com/pkg/errors
和標準庫的error
完全兼容手销,可以先替換、后續(xù)改造歷史遺留的代碼- 一定要注意打印
error
的堆棧需要用%+v
图张,而原來的%v
依舊為普通字符串方法锋拖;同時也要注意日志采集工具是否支持多行匹配
我是簡凡诈悍,一個勵志用最簡單的語言,描述最復(fù)雜問題的新時代農(nóng)民工。求點贊暖呕,求關(guān)注刹勃,如果你對此篇文章有什么疑惑,歡迎在我的微信公眾號中留言慕趴,我還可以為你提供以下幫助:
- 幫助建立自己的知識體系
- 互聯(lián)網(wǎng)真實高并發(fā)場景實戰(zhàn)講解
- 不定期分享Golang、Java相關(guān)業(yè)內(nèi)的經(jīng)典場景實踐
我的博客:https://besthpt.github.io/
微信公眾號:"簡凡丶"