Gin框架 - 自定義錯誤處理

概述

不少讀者在後臺向我要 Gin 框架實戰系列的 Demo 源碼,在這裏再說明一下,源碼我都更新到 GitHub 上,地址:https://github.com/xinliangnote/Go

開始今天的文章,爲何要自定義錯誤處理?默認的錯誤處理方式是什麼?

那好,我們就先說下默認的錯誤處理。

默認的錯誤處理是 errors.New("錯誤信息"),這個信息經過 error 類型的返回值進行返回。

舉個簡單的例子:

   php

func hello(name string) (str string, err error) {    
        if name == "" {    
            err = errors.New("name 不能爲空")    
            return    
        }    
        str = fmt.Sprintf("hello: %s", name)    
        return    
    }

 



當調用這個方法時:

 git

  var name = ""    
    str, err :=  hello(name)    
    if err != nil {    
        fmt.Println(err.Error())    
        return    
    }

 



這就是默認的錯誤處理,下面還會用這個例子進行說。

這個默認的錯誤處理,只是獲得了一個錯誤信息的字符串。

然而...

我還想獲得發生錯誤時的 時間、 文件名、 方法名、 行號 等信息。

我還想獲得錯誤時進行告警,好比 短信告警、 郵件告警、 微信告警 等。

我還想調用的時候,不那麼複雜,就和默認錯誤處理相似,好比:

    alarm.WeChat("錯誤信息")    
    return

這樣,咱們就獲得了咱們想要的信息( 時間、 文件名、 方法名、 行號),並經過 微信 的方式進行告警通知咱們。

同理, alarm.Email("錯誤信息")、 alarm.Sms("錯誤信息") 咱們獲得的信息是同樣的,只是告警方式不一樣而已。

還要保證,咱們業務邏輯中,獲取錯誤的時候,只獲取錯誤信息便可。

上面這些想出來的,就是今天要實現的,自定義錯誤處理,咱們就實現以前,先說下 Go 的錯誤處理。

錯誤處理

 github

  package main    
    import (    
        "errors"    
        "fmt"    
    )    
    func hello(name string) (str string, err error) {    
        if name == "" {    
            err = errors.New("name 不能爲空")    
            return    
        }    
        str = fmt.Sprintf("hello: %s", name)    
        return    
    }    
    func main() {    
        var name = ""    
        fmt.Println("param:", name)    
        str, err := hello(name)    
        if err != nil {    
            fmt.Println(err.Error())    
            return    
        }    
        fmt.Println(str)    
    }

 



輸出:

json

    param: Tom    
    hello: Tom

 



當 name = "" 時,輸出:

 微信

  param:    
    name 不能爲空

 


建議每一個函數都要有錯誤處理,error 應該爲最後一個返回值。

我們一塊兒看下官方 errors.go

 框架

  // Copyright 2011 The Go Authors. All rights reserved.    
    // Use of this source code is governed by a BSD-style    
    // license that can be found in the LICENSE file.    
    // Package errors implements functions to manipulate errors.    
    package errors    
    // New returns an error that formats as the given text.    
    func New(text string) error {    
        return &errorString{text}    
    }    
    // errorString is a trivial implementation of error.    
    type errorString struct {    
        s string    
    }    
    func (e *errorString) Error() string {    
        return e.s    
    }

 



上面的代碼,並不複雜,參照上面的,我們進行寫一個自定義錯誤處理。

自定義錯誤處理

我們定義一個 alarm.go,用於處理告警。

廢話很少說,直接看代碼。

  異步

 package alarm    
    import (    
        "encoding/json"    
        "fmt"    
        "ginDemo/common/function"    
        "path/filepath"    
        "runtime"    
        "strings"    
    )    
    type errorString struct {    
        s string    
    }    
    type errorInfo struct {    
        Time     string `json:"time"`    
        Alarm    string `json:"alarm"`    
        Message  string `json:"message"`    
        Filename string `json:"filename"`    
        Line     int    `json:"line"`    
        Funcname string `json:"funcname"`    
    }    
    func (e *errorString) Error() string {    
        return e.s    
    }    
    func New (text string) error {    
        alarm("INFO", text)    
        return &errorString{text}    
    }    
    // 發郵件    
    func Email (text string) error {    
        alarm("EMAIL", text)    
        return &errorString{text}    
    }    
    // 發短信    
    func Sms (text string) error {    
        alarm("SMS", text)    
        return &errorString{text}    
    }    
    // 發微信    
    func WeChat (text string) error {    
        alarm("WX", text)    
        return &errorString{text}    
    }    
    // 告警方法    
    func  alarm(level string, str string) {    
        // 當前時間    
        currentTime := function.GetTimeStr()    
        // 定義 文件名、行號、方法名    
        fileName, line, functionName := "?", 0 , "?"    
        pc, fileName, line, ok := runtime.Caller(2)    
        if ok {    
            functionName = runtime.FuncForPC(pc).Name()    
            functionName = filepath.Ext(functionName)    
            functionName = strings.TrimPrefix(functionName, ".")    
        }    
        var msg = errorInfo {    
            Time     : currentTime,    
            Alarm    : level,    
            Message  : str,    
            Filename : fileName,    
            Line     : line,    
            Funcname : functionName,    
        }    
        jsons, errs := json.Marshal(msg)    
        if errs != nil {    
            fmt.Println("json marshal error:", errs)    
        }    
        errorJsonInfo := string(jsons)    
        fmt.Println(errorJsonInfo)    
        if level == "EMAIL" {    
            // 執行發郵件    
        } else if level == "SMS" {    
            // 執行發短信    
        } else if level == "WX" {    
            // 執行發微信    
        } else if level == "INFO" {    
            // 執行記日誌    
        }    
    }

 



看下如何調用:

  函數

 package v1    
    import (    
        "fmt"    
        "ginDemo/common/alarm"    
        "ginDemo/entity"    
        "github.com/gin-gonic/gin"    
        "net/http"    
    )    
    func AddProduct(c *gin.Context)  {    
        // 獲取 Get 參數    
        name := c.Query("name")    
        var res = entity.Result{}    
        str, err := hello(name)    
        if err != nil {    
            res.SetCode(entity.CODE_ERROR)    
            res.SetMessage(err.Error())    
            c.JSON(http.StatusOK, res)    
            c.Abort()    
            return    
        }    
        res.SetCode(entity.CODE_SUCCESS)    
        res.SetMessage(str)    
        c.JSON(http.StatusOK, res)    
    }    
    func hello(name string) (str string, err error) {    
        if name == "" {    
            err = alarm.WeChat("name 不能爲空")    
            return    
        }    
        str = fmt.Sprintf("hello: %s", name)    
        return    
    }

 

 

訪問:http://localhost:8080/v1/product/add?name=a

    {    
        "code": 1,    
        "msg": "hello: a",    
        "data": null    
    }

 


未拋出錯誤,不會輸出信息。

ui

訪問:http://localhost:8080/v1/product/add

    {    
        "code": -1,    
        "msg": "name 不能爲空",    
        "data": null    
    }

 


拋出了錯誤,輸出信息以下:

{"time":"2019-07-23 22:19:17","alarm":"WX","message":"name 不能爲空","filename":"絕對路徑/ginDemo/router/v1/product.go","line":33,"funcname":"hello"}

可能這會有同窗說:「用上一篇分享的數據綁定和驗證,將傳入的參數進行 binding:"required" 也能夠實現呀」。

我只能說:「同窗呀,你不理解個人良苦用心,這只是個例子,你們能夠在一些複雜的業務邏輯判斷場景中使用自定義錯誤處理」。

到這裏,報錯時咱們收到了 時間、 錯誤信息、 文件名、 行號、 方法名 了。

調用起來,也比較簡單。

雖然標記了告警方式,仍是沒有進行告警通知呀。

我想說,在這裏存儲數據到隊列中,再執行異步任務具體去消耗,這塊就不實現了,你們能夠去完善。

讀取 文件名、 方法名、 行號 使用的是 runtime.Caller()。

咱們還知道,Go 有 panic 和 recover,它們是幹什麼的呢,接下來我們就說說。

panic 和 recover

當程序不能繼續運行的時候,才應該使用 panic 拋出錯誤。

當程序發生 panic 後,在 defer(延遲函數) 內部能夠調用 recover 進行控制,不過有個前提條件,只有在相同的 Go 協程中才能夠。

panic 分兩個,一種是有意拋出的,一種是無心的寫程序馬虎形成的,我們一個個說。

有意拋出的 panic:

  this

 package main    
    import (    
        "fmt"    
    )    
    func main() {    
        fmt.Println("-- 1 --")    
        defer func() {    
            if r := recover(); r != nil {    
                fmt.Printf("panic: %s\n", r)    
            }    
            fmt.Println("-- 2 --")    
        }()    
        panic("i am panic")    
    }

 



輸出:

 

   -- 1 --    
    panic: i am panic    
    -- 2 --

 


無心拋出的 panic:

  

 package main    
    import (    
        "fmt"    
    )    
    func main() {    
        fmt.Println("-- 1 --")    
        defer func() {    
            if r := recover(); r != nil {    
                fmt.Printf("panic: %s\n", r)    
            }    
            fmt.Println("-- 2 --")    
        }()    
        var slice = [] int {1, 2, 3, 4, 5}    
        slice[6] = 6    
    }

 


輸出:

 

  -- 1 --    
    panic: runtime error: index out of range    
    -- 2 --

 



上面的兩個咱們都經過 recover 捕獲到了,那咱們如何在 Gin 框架中使用呢?若是收到 panic 時,也想進行告警怎麼實現呢?

既然想實現告警,先在 ararm.go 中定義一個 Panic() 方法,當項目發生 panic 異常時,調用這個方法,這樣就實現告警了。

 

   // Panic 異常    
    func Panic (text string) error {    
        alarm("PANIC", text)    
        return &errorString{text}    
    }

 


那咱們怎麼捕獲到呢?

使用中間件進行捕獲,寫一個 recover 中間件。

    package recover    
    import (    
        "fmt"    
        "ginDemo/common/alarm"    
        "github.com/gin-gonic/gin"    
    )    
    func Recover()  gin.HandlerFunc {    
        return func(c *gin.Context) {    
            defer func() {    
                if r := recover(); r != nil {    
                    alarm.Panic(fmt.Sprintf("%s", r))    
                }    
            }()    
            c.Next()    
        }    
    }

 



路由調用中間件:

    r.Use(logger.LoggerToFile(), recover.Recover())    
    //Use 能夠傳遞多箇中間件。

 



驗證下吧,我們先拋出兩個異常,看看可否捕獲到?

仍是修改 product.go 這個文件吧。

有意拋出 panic:

   

package v1    
    import (    
        "fmt"    
        "ginDemo/entity"    
        "github.com/gin-gonic/gin"    
        "net/http"    
    )    
    func AddProduct(c *gin.Context)  {    
        // 獲取 Get 參數    
        name := c.Query("name")    
        var res = entity.Result{}    
        str, err := hello(name)    
        if err != nil {    
            res.SetCode(entity.CODE_ERROR)    
            res.SetMessage(err.Error())    
            c.JSON(http.StatusOK, res)    
            c.Abort()    
            return    
        }    
        res.SetCode(entity.CODE_SUCCESS)    
        res.SetMessage(str)    
        c.JSON(http.StatusOK, res)    
    }    
    func hello(name string) (str string, err error) {    
        if name == "" {    
            // 有意拋出 panic    
            panic("i am panic")    
            return    
        }    
        str = fmt.Sprintf("hello: %s", name)    
        return    
    }

 



訪問:http://localhost:8080/v1/product/add

界面是空白的。

拋出了異常,輸出信息以下:

{"time":"2019-07-23 22:42:37","alarm":"PANIC","message":"i am panic","filename":"絕對路徑/ginDemo/middleware/recover/recover.go","line":13,"funcname":"1"}

很顯然,定位的文件名、方法名、行號不是咱們想要的。

須要調整 runtime.Caller(2),這個代碼在 alarm.go的alarm 方法中。

將 2 調整成 4 ,看下輸出信息:

{"time":"2019-07-23 22:45:24","alarm":"PANIC","message":"i am panic","filename":"絕對路徑/ginDemo/router/v1/product.go","line":33,"funcname":"hello"}

這就對了。

無心拋出 panic:

 

   // 上面代碼不變    
    func hello(name string) (str string, err error) {    
        if name == "" {    
            // 無心拋出 panic    
            var slice = [] int {1, 2, 3, 4, 5}    
            slice[6] = 6    
            return    
        }    
        str = fmt.Sprintf("hello: %s", name)    
        return    
    }

 



訪問:http://localhost:8080/v1/product/add

界面是空白的。

拋出了異常,輸出信息以下:

{"time":"2019-07-23 22:50:06","alarm":"PANIC","message":"runtime error: index out of range","filename":"絕對路徑/runtime/panic.go","line":44,"funcname":"panicindex"}

很顯然,定位的文件名、方法名、行號也不是咱們想要的。

將 4 調整成 5 ,看下輸出信息:

{"time":"2019-07-23 22:55:27","alarm":"PANIC","message":"runtime error: index out of range","filename":"絕對路徑/ginDemo/router/v1/product.go","line":34,"funcname":"hello"}

這就對了。

奇怪了,這是爲何?

在這裏,有必要說下 runtime.Caller(skip) 了。

skip 指的調用的深度。

爲 0 時,打印當前調用文件及行數。

爲 1 時,打印上級調用的文件及行數。

依次類推...

在這塊,調用的時候須要注意下,我如今尚未好的解決方案。

我是將 skip(調用深度),當一個參數傳遞進去。

好比:

    // 發微信    
    func WeChat (text string) error {    
        alarm("WX", text, 2)    
        return &errorString{text}    
    }    
    // Panic 異常    
    func Panic (text string) error {    
        alarm("PANIC", text, 5)    
        return &errorString{text}    
    }

 

具體的代碼就不貼了。可是,有意拋出 Panic 和 無心拋出 Panic 的調用深度又不一樣,怎麼辦?一、儘可能將有意拋出的 Panic 改爲拋出錯誤的方式。二、想其餘辦法搞定它。

相關文章
相關標籤/搜索