概述
不少讀者在後臺向我要 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 改爲拋出錯誤的方式。二、想其餘辦法搞定它。