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 改为抛出错误的方式。二、想其余办法搞定它。

相关文章
相关标签/搜索