概述 开始今天的文章,为什么要自定义错误处理?默认的错误处理方式是什么?
那好,咱们就先说下默认的错误处理。
默认的错误处理是 errors.New("错误信息")
,这个信息通过 error 类型的返回值进行返回。
举个简单的例子:
1 2 3 4 5 6 7 8 func hello(name string) (str string, err error) { if name == "" { err = errors.New("name 不能为空") return } str = fmt.Sprintf("hello: %s", name) return }
当调用这个方法时:
1 2 3 4 5 6 var name = "" str, err := hello(name) if err != nil { fmt.Println(err.Error()) return }
这就是默认的错误处理,下面还会用这个例子进行说。
这个默认的错误处理,只是得到了一个错误信息的字符串。
然而…
我还想得到发生错误时的 时间
、文件名
、方法名
、行号
等信息。
我还想得到错误时进行告警,比如 短信告警
、邮件告警
、微信告警
等。
我还想调用的时候,不那么复杂,就和默认错误处理类似,比如:
1 2 alarm.WeChat("错误信息") return
这样,我们就得到了我们想要的信息(时间
、文件名
、方法名
、行号
),并通过 微信
的方式进行告警通知我们。
同理,alarm.Email("错误信息")
、alarm.Sms("错误信息")
我们得到的信息是一样的,只是告警方式不同而已。
还要保证,我们业务逻辑中,获取错误的时候,只获取错误信息即可。
上面这些想出来的,就是今天要实现的,自定义错误处理,我们就实现之前,先说下 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 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) }
输出:
当 name = “” 时,输出:
建议每个函数都要有错误处理,error 应该为最后一个返回值。
咱们一起看下官方 errors.go
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 // 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,用于处理告警。
废话不多说,直接看代码。
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 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" { // 执行记日志 } }
看下如何调用:
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 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
1 2 3 4 5 { "code": 1, "msg": "hello: a", "data": null }
未抛出错误,不会输出信息。
访问:http://localhost:8080/v1/product/add
1 2 3 4 5 { "code": -1, "msg": "name 不能为空", "data": null }
抛出了错误,输出信息如下:
1 {"time":"2019-07-23 22:19:17","alarm":"WX","message":"name 不能为空","filename":"绝对路径/ginDemo/router/v1/product.go","line":33,"funcname":"hello"}
这只是个例子,大家可以在一些复杂的业务逻辑判断场景中使用自定义错误处理”。
到这里,报错时我们收到了 时间
、错误信息
、文件名
、行号
、方法名
了。
调用起来,也比较简单。
虽然标记了告警方式,还是没有进行告警通知呀。
我想说,在这里存储数据到队列中,再执行异步任务具体去消耗,这块就不实现了,大家可以去完善。
读取 文件名
、方法名
、行号
使用的是 runtime.Caller()
。
我们还知道,Go 有 panic
和 recover
,它们是干什么的呢,接下来咱们就说说。
panic 和 recover 当程序不能继续运行的时候,才应该使用 panic 抛出错误。
当程序发生 panic 后,在 defer(延迟函数) 内部可以调用 recover 进行控制,不过有个前提条件,只有在相同的 Go 协程中才可以。
panic 分两个,一种是有意抛出的,一种是无意的写程序马虎造成的,咱们一个个说。
有意抛出的 panic:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 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 2 3 -- 1 -- panic: i am panic -- 2 --
无意抛出的 panic:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 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 2 3 -- 1 -- panic: runtime error: index out of range -- 2 --
上面的两个我们都通过 recover
捕获到了,那我们如何在 Gin 框架中使用呢?如果收到 panic
时,也想进行告警怎么实现呢?
既然想实现告警,先在 ararm.go 中定义一个 Panic()
方法,当项目发生 panic
异常时,调用这个方法,这样就实现告警了。
1 2 3 4 5 // Panic 异常 func Panic (text string) error { alarm("PANIC", text) return &errorString{text} }
那我们怎么捕获到呢?
使用中间件进行捕获,写一个 recover
中间件。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 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() } }
路由调用中间件:
1 2 3 r.Use(logger.LoggerToFile(), recover.Recover()) //Use 可以传递多个中间件。
验证下吧,咱们先抛出两个异常,看看能否捕获到?
还是修改 product.go 这个文件吧。
有意抛出 panic:
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 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
界面是空白的。
抛出了异常,输出信息如下:
1 {"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 ,看下输出信息:
1 {"time":"2019-07-23 22:45:24","alarm":"PANIC","message":"i am panic","filename":"绝对路径/ginDemo/router/v1/product.go","line":33,"funcname":"hello"}
这就对了。
无意抛出 panic:
1 2 3 4 5 6 7 8 9 10 11 12 // 上面代码不变 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
界面是空白的。
抛出了异常,输出信息如下:
1 {"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 ,看下输出信息:
1 {"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(调用深度),当一个参数传递进去。
比如:
1 2 3 4 5 6 7 8 9 10 11 // 发微信 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 的调用深度又不同,怎么办?
1、尽量将有意抛出的 Panic 改成抛出错误的方式。
2、想其他办法搞定它。
就到这吧。