经验首页 前端设计 程序设计 Java相关 移动开发 数据库/运维 软件/图像 大数据/云计算 其他经验
当前位置:技术经验 » 程序设计 » PHP » 查看文章
Gin框架 - 自定义错误处理 - it-world
来源:cnblogs  作者:it-world  时间:2019/11/15 9:00:45  对本文有异议

概述

很多读者在后台向我要 Gin 框架实战系列的 Demo 源码,在这里再说明一下,源码我都更新到 GitHub 上,地址:https://github.com/xinliangnote/Go

开始今天的文章,为什么要自定义错误处理?默认的错误处理方式是什么?

那好,咱们就先说下默认的错误处理。

默认的错误处理是 errors.New("错误信息"),这个信息通过 error 类型的返回值进行返回。

举个简单的例子:

   

  1. func hello(name string) (str string, err error) {
  2. if name == "" {
  3. err = errors.New("name 不能为空")
  4. return
  5. }
  6. str = fmt.Sprintf("hello: %s", name)
  7. return
  8. }

 



当调用这个方法时:

 

  1. var name = ""
  2. str, err := hello(name)
  3. if err != nil {
  4. fmt.Println(err.Error())
  5. return
  6. }

 



这就是默认的错误处理,下面还会用这个例子进行说。

这个默认的错误处理,只是得到了一个错误信息的字符串。

然而...

我还想得到发生错误时的 时间、 文件名、 方法名、 行号 等信息。

我还想得到错误时进行告警,比如 短信告警、 邮件告警、 微信告警 等。

我还想调用的时候,不那么复杂,就和默认错误处理类似,比如:

    alarm.WeChat("错误信息")    
    return

这样,我们就得到了我们想要的信息( 时间、 文件名、 方法名、 行号),并通过 微信 的方式进行告警通知我们。

同理, alarm.Email("错误信息")、 alarm.Sms("错误信息") 我们得到的信息是一样的,只是告警方式不同而已。

还要保证,我们业务逻辑中,获取错误的时候,只获取错误信息即可。

上面这些想出来的,就是今天要实现的,自定义错误处理,我们就实现之前,先说下 Go 的错误处理。

错误处理

 

  1. package main
  2. import (
  3. "errors"
  4. "fmt"
  5. )
  6. func hello(name string) (str string, err error) {
  7. if name == "" {
  8. err = errors.New("name 不能为空")
  9. return
  10. }
  11. str = fmt.Sprintf("hello: %s", name)
  12. return
  13. }
  14. func main() {
  15. var name = ""
  16. fmt.Println("param:", name)
  17. str, err := hello(name)
  18. if err != nil {
  19. fmt.Println(err.Error())
  20. return
  21. }
  22. fmt.Println(str)
  23. }

 



输出:

  1. param: Tom
  2. hello: Tom

 



当 name = "" 时,输出:

 

  1. param:
  2. name 不能为空

 


建议每个函数都要有错误处理,error 应该为最后一个返回值。

咱们一起看下官方 errors.go

 

  1. // Copyright 2011 The Go Authors. All rights reserved.
  2. // Use of this source code is governed by a BSD-style
  3. // license that can be found in the LICENSE file.
  4. // Package errors implements functions to manipulate errors.
  5. package errors
  6. // New returns an error that formats as the given text.
  7. func New(text string) error {
  8. return &errorString{text}
  9. }
  10. // errorString is a trivial implementation of error.
  11. type errorString struct {
  12. s string
  13. }
  14. func (e *errorString) Error() string {
  15. return e.s
  16. }

 



上面的代码,并不复杂,参照上面的,咱们进行写一个自定义错误处理。

自定义错误处理

咱们定义一个 alarm.go,用于处理告警。

废话不多说,直接看代码。

  

  1. package alarm
  2. import (
  3. "encoding/json"
  4. "fmt"
  5. "ginDemo/common/function"
  6. "path/filepath"
  7. "runtime"
  8. "strings"
  9. )
  10. type errorString struct {
  11. s string
  12. }
  13. type errorInfo struct {
  14. Time string `json:"time"`
  15. Alarm string `json:"alarm"`
  16. Message string `json:"message"`
  17. Filename string `json:"filename"`
  18. Line int `json:"line"`
  19. Funcname string `json:"funcname"`
  20. }
  21. func (e *errorString) Error() string {
  22. return e.s
  23. }
  24. func New (text string) error {
  25. alarm("INFO", text)
  26. return &errorString{text}
  27. }
  28. // 发邮件
  29. func Email (text string) error {
  30. alarm("EMAIL", text)
  31. return &errorString{text}
  32. }
  33. // 发短信
  34. func Sms (text string) error {
  35. alarm("SMS", text)
  36. return &errorString{text}
  37. }
  38. // 发微信
  39. func WeChat (text string) error {
  40. alarm("WX", text)
  41. return &errorString{text}
  42. }
  43. // 告警方法
  44. func alarm(level string, str string) {
  45. // 当前时间
  46. currentTime := function.GetTimeStr()
  47. // 定义 文件名、行号、方法名
  48. fileName, line, functionName := "?", 0 , "?"
  49. pc, fileName, line, ok := runtime.Caller(2)
  50. if ok {
  51. functionName = runtime.FuncForPC(pc).Name()
  52. functionName = filepath.Ext(functionName)
  53. functionName = strings.TrimPrefix(functionName, ".")
  54. }
  55. var msg = errorInfo {
  56. Time : currentTime,
  57. Alarm : level,
  58. Message : str,
  59. Filename : fileName,
  60. Line : line,
  61. Funcname : functionName,
  62. }
  63. jsons, errs := json.Marshal(msg)
  64. if errs != nil {
  65. fmt.Println("json marshal error:", errs)
  66. }
  67. errorJsonInfo := string(jsons)
  68. fmt.Println(errorJsonInfo)
  69. if level == "EMAIL" {
  70. // 执行发邮件
  71. } else if level == "SMS" {
  72. // 执行发短信
  73. } else if level == "WX" {
  74. // 执行发微信
  75. } else if level == "INFO" {
  76. // 执行记日志
  77. }
  78. }

 



看下如何调用:

  

  1. package v1
  2. import (
  3. "fmt"
  4. "ginDemo/common/alarm"
  5. "ginDemo/entity"
  6. "github.com/gin-gonic/gin"
  7. "net/http"
  8. )
  9. func AddProduct(c *gin.Context) {
  10. // 获取 Get 参数
  11. name := c.Query("name")
  12. var res = entity.Result{}
  13. str, err := hello(name)
  14. if err != nil {
  15. res.SetCode(entity.CODE_ERROR)
  16. res.SetMessage(err.Error())
  17. c.JSON(http.StatusOK, res)
  18. c.Abort()
  19. return
  20. }
  21. res.SetCode(entity.CODE_SUCCESS)
  22. res.SetMessage(str)
  23. c.JSON(http.StatusOK, res)
  24. }
  25. func hello(name string) (str string, err error) {
  26. if name == "" {
  27. err = alarm.WeChat("name 不能为空")
  28. return
  29. }
  30. str = fmt.Sprintf("hello: %s", name)
  31. return
  32. }

 

 

  1. 访问:http://localhost:8080/v1/product/add?name=a
  2.  
  3. {
  4. "code": 1,
  5. "msg": "hello: a",
  6. "data": null
  7. }

 


未抛出错误,不会输出信息。

  1. 访问:http://localhost:8080/v1/product/add
  2.  
  3. {
  4. "code": -1,
  5. "msg": "name 不能为空",
  6. "data": null
  7. }

 


抛出了错误,输出信息如下:

{"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:

  

  1. package main
  2. import (
  3. "fmt"
  4. )
  5. func main() {
  6. fmt.Println("-- 1 --")
  7. defer func() {
  8. if r := recover(); r != nil {
  9. fmt.Printf("panic: %s\n", r)
  10. }
  11. fmt.Println("-- 2 --")
  12. }()
  13. panic("i am panic")
  14. }

 



输出:

 

  1. -- 1 --
  2. panic: i am panic
  3. -- 2 --

 


无意抛出的 panic:

  

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

 


输出:

 

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

 



上面的两个我们都通过 recover 捕获到了,那我们如何在 Gin 框架中使用呢?如果收到 panic 时,也想进行告警怎么实现呢?

既然想实现告警,先在 ararm.go 中定义一个 Panic() 方法,当项目发生 panic 异常时,调用这个方法,这样就实现告警了。

 

  1. // Panic 异常
  2. func Panic (text string) error {
  3. alarm("PANIC", text)
  4. return &errorString{text}
  5. }

 


那我们怎么捕获到呢?

使用中间件进行捕获,写一个 recover 中间件。

  1. package recover
  2. import (
  3. "fmt"
  4. "ginDemo/common/alarm"
  5. "github.com/gin-gonic/gin"
  6. )
  7. func Recover() gin.HandlerFunc {
  8. return func(c *gin.Context) {
  9. defer func() {
  10. if r := recover(); r != nil {
  11. alarm.Panic(fmt.Sprintf("%s", r))
  12. }
  13. }()
  14. c.Next()
  15. }
  16. }

 



路由调用中间件:

  1. r.Use(logger.LoggerToFile(), recover.Recover())
  2. //Use 可以传递多个中间件。

 



验证下吧,咱们先抛出两个异常,看看能否捕获到?

还是修改 product.go 这个文件吧。

有意抛出 panic:

   

  1. package v1
  2. import (
  3. "fmt"
  4. "ginDemo/entity"
  5. "github.com/gin-gonic/gin"
  6. "net/http"
  7. )
  8. func AddProduct(c *gin.Context) {
  9. // 获取 Get 参数
  10. name := c.Query("name")
  11. var res = entity.Result{}
  12. str, err := hello(name)
  13. if err != nil {
  14. res.SetCode(entity.CODE_ERROR)
  15. res.SetMessage(err.Error())
  16. c.JSON(http.StatusOK, res)
  17. c.Abort()
  18. return
  19. }
  20. res.SetCode(entity.CODE_SUCCESS)
  21. res.SetMessage(str)
  22. c.JSON(http.StatusOK, res)
  23. }
  24. func hello(name string) (str string, err error) {
  25. if name == "" {
  26. // 有意抛出 panic
  27. panic("i am panic")
  28. return
  29. }
  30. str = fmt.Sprintf("hello: %s", name)
  31. return
  32. }

 



访问: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:

 

  1. // 上面代码不变
  2. func hello(name string) (str string, err error) {
  3. if name == "" {
  4. // 无意抛出 panic
  5. var slice = [] int {1, 2, 3, 4, 5}
  6. slice[6] = 6
  7. return
  8. }
  9. str = fmt.Sprintf("hello: %s", name)
  10. return
  11. }

 



访问: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(调用深度),当一个参数传递进去。

比如:

  1. // 发微信
  2. func WeChat (text string) error {
  3. alarm("WX", text, 2)
  4. return &errorString{text}
  5. }
  6. // Panic 异常
  7. func Panic (text string) error {
  8. alarm("PANIC", text, 5)
  9. return &errorString{text}
  10. }

 



具体的代码就不贴了。

但是,有意抛出 Panic 和 无意抛出 Panic 的调用深度又不同,怎么办?

1、尽量将有意抛出的 Panic 改成抛出错误的方式。

2、想其他办法搞定它。


原文链接:http://www.cnblogs.com/it-3327/p/11862859.html

 友情链接:直通硅谷  点职佳  北美留学生论坛

本站QQ群:前端 618073944 | Java 606181507 | Python 626812652 | C/C++ 612253063 | 微信 634508462 | 苹果 692586424 | C#/.net 182808419 | PHP 305140648 | 运维 608723728

W3xue 的所有内容仅供测试,对任何法律问题及风险不承担任何责任。通过使用本站内容随之而来的风险与本站无关。
关于我们  |  意见建议  |  捐助我们  |  报错有奖  |  广告合作、友情链接(目前9元/月)请联系QQ:27243702 沸活量
皖ICP备17017327号-2 皖公网安备34020702000426号