最佳實踐之Golang錯誤處理

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é)

  1. CustomError 作為全局 error 的底層實現(xiàn)缝呕,保存具體的錯誤碼和錯誤信息澳窑;
  2. CustomError向上返回錯誤時,第一次先用Wrap初始化堆棧供常,后續(xù)用WithMessage增加堆棧信息摊聋;
  3. error中解析具體錯誤時,用errors.As提取出CustomError栈暇,其中的錯誤碼和錯誤信息可以傳入到具體的API接口中麻裁;
  4. 要判斷error是否為指定的錯誤時,用errors.Is + Handler Error的方法源祈,處理一些特定情況下的邏輯煎源;

Tips:

  1. 不要一直用errors.Wrap來反復(fù)包裝錯誤,堆棧信息會爆炸香缺,具體情況可自行測試了解
  2. 利用go generate可以大量簡化初始化Erro重復(fù)的工作
  3. github.com/pkg/errors和標準庫的error完全兼容手销,可以先替換、后續(xù)改造歷史遺留的代碼
  4. 一定要注意打印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/
微信公眾號:"簡凡丶"

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末鄙陡,一起剝皮案震驚了整個濱河市冕房,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌趁矾,老刑警劉巖耙册,帶你破解...
    沈念sama閱讀 217,542評論 6 504
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異毫捣,居然都是意外死亡详拙,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,822評論 3 394
  • 文/潘曉璐 我一進店門蔓同,熙熙樓的掌柜王于貴愁眉苦臉地迎上來饶辙,“玉大人,你說我怎么就攤上這事斑粱∑浚” “怎么了?”我有些...
    開封第一講書人閱讀 163,912評論 0 354
  • 文/不壞的土叔 我叫張陵则北,是天一觀的道長矿微。 經(jīng)常有香客問我,道長尚揣,這世上最難降的妖魔是什么涌矢? 我笑而不...
    開封第一講書人閱讀 58,449評論 1 293
  • 正文 為了忘掉前任,我火速辦了婚禮快骗,結(jié)果婚禮上娜庇,老公的妹妹穿的比我還像新娘。我一直安慰自己方篮,他們只是感情好思灌,可當我...
    茶點故事閱讀 67,500評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著恭取,像睡著了一般泰偿。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上蜈垮,一...
    開封第一講書人閱讀 51,370評論 1 302
  • 那天耗跛,我揣著相機與錄音裕照,去河邊找鬼。 笑死调塌,一個胖子當著我的面吹牛晋南,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播羔砾,決...
    沈念sama閱讀 40,193評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼负间,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了姜凄?” 一聲冷哼從身側(cè)響起政溃,我...
    開封第一講書人閱讀 39,074評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎态秧,沒想到半個月后董虱,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,505評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡申鱼,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,722評論 3 335
  • 正文 我和宋清朗相戀三年愤诱,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片捐友。...
    茶點故事閱讀 39,841評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡淫半,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出匣砖,到底是詐尸還是另有隱情科吭,我是刑警寧澤,帶...
    沈念sama閱讀 35,569評論 5 345
  • 正文 年R本政府宣布脆粥,位于F島的核電站砌溺,受9級特大地震影響影涉,放射性物質(zhì)發(fā)生泄漏变隔。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,168評論 3 328
  • 文/蒙蒙 一蟹倾、第九天 我趴在偏房一處隱蔽的房頂上張望匣缘。 院中可真熱鬧,春花似錦鲜棠、人聲如沸肌厨。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,783評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽柑爸。三九已至,卻和暖如春盒音,著一層夾襖步出監(jiān)牢的瞬間表鳍,已是汗流浹背馅而。 一陣腳步聲響...
    開封第一講書人閱讀 32,918評論 1 269
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留譬圣,地道東北人瓮恭。 一個月前我還...
    沈念sama閱讀 47,962評論 2 370
  • 正文 我出身青樓,卻偏偏與公主長得像厘熟,于是被迫代替她去往敵國和親屯蹦。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 44,781評論 2 354

推薦閱讀更多精彩內(nèi)容

  • 在實際工程項目中绳姨,總是通過程序的錯誤信息快速定位問題登澜,但是又不希望錯誤處理代碼寫的冗余而又啰嗦。Go語言沒有提供像...
    drunkery閱讀 856評論 0 0
  • 最佳實踐 panic 程序在啟動時就缆,如果有強依賴的服務(wù)出現(xiàn)故障時帖渠,需要panic退出 在程序啟動時,如果發(fā)現(xiàn)有配置...
    匿名回復(fù)123閱讀 1,238評論 0 1
  • 問題 錯誤處理竭宰,是非常重要的空郊。在go語言中,錯誤處理被設(shè)計的十分簡單切揭。如果做得好狞甚,會在排查問題等方面很有幫助;如果...
    每天一個俯臥撐閱讀 433評論 0 1
  • 錯誤處理 在實際工程項目中廓旬,我們希望通過程序的錯誤信息快速定位問題哼审,但是又不喜歡錯誤處理代碼寫的冗余而又啰嗦。Go...
    那錢有著落嗎閱讀 416評論 0 0
  • 最近對項目進行了重構(gòu)孕豹,將以前詬病的代碼全部刪了涩盾,重新寫了,這里介紹下Lumen里面如何簡單的攔截掉所有錯誤励背,達到“...
    yieldHL閱讀 3,167評論 0 1