Gin框架 - 自定义错误处理
概述
很多读者在后台向我要 Gin 框架实战系列的 Demo 源码,在这里再说明一下,源码我都更新到 GitHub 上,地址:https://github.com/xinliangnote/Go
开始今天的文章,为什么要自定义错误处理?默认的错误处理方式是什么?
那好,咱们就先说下默认的错误处理。
默认的错误处理是 errors.New("错误信息"),这个信息通过 error 类型的返回值进行返回。
举个简单的例子:
func hello(name string) (str string, err error) {
if name == "" {
err = errors.New("name 不能为空")
return
}
str = fmt.Sprintf("hello: %s", name)
return
}
当调用这个方法时:
var name = ""
str, err := hello(name)
if err != nil {
fmt.Println(err.Error())
return
}
这就是默认的错误处理,下面还会用这个例子进行说。
这个默认的错误处理,只是得到了一个错误信息的字符串。
然而...
我还想得到发生错误时的 时间、文件名、方法名、行号 等信息。
我还想得到错误时进行告警,比如 短信告警、邮件告警、微信告警 等。
我还想调用的时候,不那么复杂,就和默认错误处理类似,比如:
alarm.WeChat("错误信息")
return
这样,我们就得到了我们想要的信息(时间、文件名、方法名、行号),并通过 微信 的方式进行告警通知我们。
同理,alarm.Email("错误信息")、alarm.Sms("错误信息") 我们得到的信息是一样的,只是告警方式不同而已。
还要保证,我们业务逻辑中,获取错误的时候,只获取错误信息即可。
上面这些想出来的,就是今天要实现的,自定义错误处理,我们就实现之前,先说下 Go 的错误处理。
错误处理
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)
}
输出:
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
}
未抛出错误,不会输出信息。
访问: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:
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 的调用深度又不同,怎么办?
1、尽量将有意抛出的 Panic 改成抛出错误的方式。
2、想其他办法搞定它。
就到这吧。
里面涉及到的代码,我会更新到 GitHub。
推荐阅读
gRPC
Gin 框架
基础篇
本文欢迎转发,转发请注明作者和出处,谢谢!
Gin框架 - 自定义错误处理的更多相关文章
- 【解决了一个小问题】gin框架中出现如下错误:"[GIN-debug] [WARNING] Headers were already written. Wanted to override status code 400 with 500"
POST到数据到一条gin框架的接口后,客户端收到400错误,并且返回了业务中返回的"decode json fail". 关键代码是: func report(c *gin.Co ...
- Flask框架 之abort、自定义错误、视图函数返回值与jsonify
一.abort函数 使用abort函数可以立即终止视图函数的执行,并可以返回给前端特定的值. abort函数的作用: 1.传递状态码,必须是标准的http状态码 2.传递响应体信息 @app.rout ...
- 基于gin框架和jwt-go中间件实现小程序用户登陆和token验证
本文核心内容是利用jwt-go中间件来开发golang webapi用户登陆模块的token下发和验证,小程序登陆功能只是一个切入点,这套逻辑同样适用于其他客户端的登陆处理. 小程序登陆逻辑 小程序的 ...
- [系列] Gin框架 - 数据绑定和验证
目录 概述 推荐阅读 概述 上篇文章分享了 Gin 框架使用 Logrus 进行日志记录,这篇文章分享 Gin 框架的数据绑定与验证. 有读者咨询我一个问题,如何让框架的运行日志不输出控制台? 解决方 ...
- Gin框架 - 数据绑定和验证
概述 上篇文章分享了 Gin 框架使用 Logrus 进行日志记录,这篇文章分享 Gin 框架的数据绑定与验证. 有读者咨询我一个问题,如何让框架的运行日志不输出控制台? 解决方案: engine : ...
- 基于gin框架搭建的一个简单的web服务
刚把go编程基础知识学习完了,学习的时间很短,可能还有的没有完全吸收.不过还是在项目中发现知识,然后在去回顾已学的知识,现在利用gin这个web框架做一个简单的CRUD操作. 1.Go Web框架的技 ...
- Gin框架系列02:路由与参数
回顾 上一节我们用Gin框架快速搭建了一个GET请求的接口,今天来学习路由和参数的获取. 请求动词 熟悉RESTful的同学应该知道,RESTful是网络应用程序的一种设计风格和开发方式,每一个URI ...
- MVC4 自定义错误页面(转)
一.概述 MVC4框架自带了定义错误页,该页面位于Shared/Error,该页面能够显示系统未能捕获的异常,如何才能使用该页面: 二.使用步骤: 1.配置WebConfig文件,在System.We ...
- ASP.NETMVC自定义错误页面真的简单吗?
Note:文章前半部分翻译自 http://benfoster.io/blog/aspnet-mvc-custom-error-pages ,着急的可直接看总结~ 如果你在设置asp.net mvc自 ...
随机推荐
- Win7 访问 数据库 慢
不让TCP/IP调谐拖累网速 在Windows Server 2008工作环境中,下载访问网络中大容量的文件内容时,我们有时会感觉到网络连接速度非常缓慢,严重的时候还会出现不能访问的现象.遭遇这类故障 ...
- ABP开发框架前后端开发系列---(8)ABP框架之Winform界面的开发过程
在前面随笔介绍的<ABP开发框架前后端开发系列---(7)系统审计日志和登录日志的管理>里面,介绍了如何改进和完善审计日志和登录日志的应用服务端和Winform客户端,由于篇幅限制,没有进 ...
- mk、cd、pwd、ls、touch、vi、cat、cp、mv的使用及命令快捷方式
1 命令提示符 1.1 [ root @ oldboyedu62 ~ ] 1 2 3 4 1:登陆系统的用户身份 2:命令分割符合 3:主机名称信息 4:显示当前所在目录路径 1.2 系统 ...
- spring 5.x 系列第13篇 —— 整合RabbitMQ (xml配置方式)
源码Gitub地址:https://github.com/heibaiying/spring-samples-for-all 一.说明 1.1 项目结构说明 本用例关于rabbitmq的整合提供简单消 ...
- FPM
https://github.com/pangudashu/php7-internal/blob/master/1/fpm.md
- Java基础篇01
01. 面向对象 --> 什么是面向对象 面向对象 面向对象程序设计,简称OOP(Object Oriented Programming). 对象: 指人们要研究的任何事物,不管是物理上具体的事 ...
- Node.js Windows Example
Firstly, download the msi file from https://nodejs.org/en/ Second, click the msi file to install nod ...
- white box白盒测试
逻辑覆盖法:语句覆盖,判定覆盖,条件覆盖,判定/条件覆盖,组合覆盖,路径覆盖 基本路径测试法:Control Flow Graphs, CFG.带箭头的边 条件覆盖:使每个判定中每个条件的可能值至少满 ...
- java中几个常见的问题
1.正确使用equals方法 Object的equals方法容易抛出空指针异常,应使用常量或确定有值的对象来调用equals方法 例如: //不能使用一个值为null的引用类型变量来调用非静态方法,否 ...
- 【POJ - 1995】Raising Modulo Numbers(快速幂)
-->Raising Modulo Numbers Descriptions: 题目一大堆,真没什么用,大致题意 Z M H A1 B1 A2 B2 A3 B3 ......... AH ...