前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Gin框架 - 自定义错误处理

Gin框架 - 自定义错误处理

作者头像
新亮
发布2019-07-29 12:49:59
1.6K0
发布2019-07-29 12:49:59
举报
文章被收录于专栏:新亮笔记新亮笔记

概述

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

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

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

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

举个简单的例子:

代码语言:javascript
复制
func hello(name string) (str string, err error) {    if name == "" {        err = errors.New("name 不能为空")        return    }    str = fmt.Sprintf("hello: %s", name)    return}

当调用这个方法时:

代码语言:javascript
复制
var name = ""str, err :=  hello(name)if err != nil {    fmt.Println(err.Error())    return}

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

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

然而...

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

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

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

代码语言:javascript
复制
alarm.WeChat("错误信息")return

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

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

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

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

错误处理

代码语言:javascript
复制
package mainimport (    "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)}

输出:

代码语言:javascript
复制
param: Tomhello: Tom

当 name = "" 时,输出:

代码语言:javascript
复制
param:name 不能为空

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

咱们一起看下官方 errors.go

代码语言:javascript
复制
// 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,用于处理告警。

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

代码语言:javascript
复制
package alarmimport (    "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" {        // 执行记日志    }}

看下如何调用:

代码语言:javascript
复制
package v1import (    "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

代码语言:javascript
复制
{    "code": 1,    "msg": "hello: a",    "data": null}

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

访问:http://localhost:8080/v1/product/add

代码语言:javascript
复制
{    "code": -1,    "msg": "name 不能为空",    "data": null}

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

代码语言:javascript
复制
{"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 有 panicrecover,它们是干什么的呢,接下来咱们就说说。

panic 和 recover

当程序不能继续运行的时候,才应该使用 panic 抛出错误。

当程序发生 panic 后,在 defer(延迟函数) 内部可以调用 recover 进行控制,不过有个前提条件,只有在相同的 Go 协程中才可以。

panic 分两个,一种是有意抛出的,一种是无意的写程序马虎造成的,咱们一个个说。

有意抛出的 panic:

代码语言:javascript
复制
package mainimport (    "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")}

输出:

代码语言:javascript
复制
-- 1 --panic: i am panic-- 2 --

无意抛出的 panic:

代码语言:javascript
复制
package mainimport (    "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}

输出:

代码语言:javascript
复制
-- 1 --panic: runtime error: index out of range-- 2 --

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

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

代码语言:javascript
复制
// Panic 异常func Panic (text string) error {    alarm("PANIC", text)    return &errorString{text}}

那我们怎么捕获到呢?

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

代码语言:javascript
复制
package recoverimport (    "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()    }}

路由调用中间件:

代码语言:javascript
复制
r.Use(logger.LoggerToFile(), recover.Recover())//Use 可以传递多个中间件。

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

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

有意抛出 panic:

代码语言:javascript
复制
package v1import (    "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

界面是空白的。

抛出了异常,输出信息如下:

代码语言:javascript
复制
{"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 ,看下输出信息:

代码语言:javascript
复制
{"time":"2019-07-23 22:45:24","alarm":"PANIC","message":"i am panic","filename":"绝对路径/ginDemo/router/v1/product.go","line":33,"funcname":"hello"}

这就对了。

无意抛出 panic:

代码语言:javascript
复制
// 上面代码不变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

界面是空白的。

抛出了异常,输出信息如下:

代码语言:javascript
复制
{"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 ,看下输出信息:

代码语言:javascript
复制
{"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(调用深度),当一个参数传递进去。

比如:

代码语言:javascript
复制
// 发微信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、想其他办法搞定它。

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2019-07-24,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 新亮笔记 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 概述
  • 错误处理
  • 自定义错误处理
  • panic 和 recover
相关产品与服务
消息队列 TDMQ
消息队列 TDMQ (Tencent Distributed Message Queue)是腾讯基于 Apache Pulsar 自研的一个云原生消息中间件系列,其中包含兼容Pulsar、RabbitMQ、RocketMQ 等协议的消息队列子产品,得益于其底层计算与存储分离的架构,TDMQ 具备良好的弹性伸缩以及故障恢复能力。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档