Gin框架入门(2)--异常捕获与日志实现

异常捕获

Go语言的异常捕获采用的是延迟处理的方法实现的,实际上就是利用defer,panic和recover三个关键字和函数来实现的。

关键字

defer关键字(函数)

这个关键字在控制语句中就有所涉及,本质上是采用一个栈的存储结构,在整个函数执行完之后,再启用这个栈,依次执行这个函数。需要注意的是以下几点:

  1. 由于是栈的结构,我们首先想到的便是先入后出,defer也是如此

    func f(){
    defer A
    defer B
    D
    defer C
    
    }
    

    执行的顺序是D,C,B,A

  2. 被defer标记的语句是放在最后执行的,所以也就衍生出defer的玩法——放在开头加一个打印语句,来判断go程的结束(原理如此,至于稍复杂的结合等待组或者响应等也大同小异)

panic函数

让程序直接崩溃的函数,用法panic("<报错信息>")

Recover

Recover只在延时函数中有效,效果是将崩溃的程序恢复过来;如果是在正常的语句中使用Recover,就会返回一个nil并且没有任何效果。

实例(没意义):

func main() {

	defer func() {
		if err := recover(); err != nil {
			fmt.Println("捕获异常:", err)
		}
	}()

	r := router.Router()

	r.Run(":9999")
	panic("雪豹毁了我的程序")

}

运行结果:

服务端正常运行!

实例:

使用这个方法,使得程序不崩溃的情况下获得异常,并且保证只是使得前端无返回内容,而引起这个服务器的崩溃

改写user.go

func (u UserController) GetList(c *gin.Context) {
	//ReturnError(c *gin.Context, code int, msg string)
	defer func() {
		if err := recover(); err != nil {
			fmt.Println("捕获异常:", err)
		}
	}()

	num1, num2 := 1, 0
	num3 := num1 / num2
	ReturnUserGetListError(c, 404, num3)
    //<common.go>
    //func ReturnUserGetListError(c *gin.Context, code int, msg int) {
	//	json := &JsonErrStruct{Code: code, Msg: msg}
	//	c.JSON(http.StatusOK, json)
	//}

}

运行结果

捕获异常: runtime error: integer divide by zero
[GIN] 2024/09/22 - 20:19:09 | 200 |            0s |       127.0.0.1 | POST     "/user/list"

在这里插入图片描述

日志

日志就是记录事件的记录表,主要分为以下三种

  1. 项目的请求日志
  2. 程序出现错误日志
  3. 程序员开发的时候自己保存的日志

封装保存日志的包

  1. 首先,创建一个pkg包,这个包的用途是防止开发时需要的工具。在pkg包下面创建一个子文件夹logger,用来存放日志工具,在这个子文件夹下创建logger.go文件
  2. 之后,导入一下我们将要使用的日志工具包,在终端命令行输入go get github.com/sirupsen/logrus

文件结构

中间件logger.go内容
在这里插入图片描述

package logger

import (
	"fmt"
	"github.com/gin-gonic/gin"
	"github.com/sirupsen/logrus"
	"io"
	"os"
	"path"
	"path/filepath"
	"runtime/debug"
	"time"
)

// 初始化日志设置
func init() {
	// 设置日志的 JSON 格式
	logrus.SetFormatter(&logrus.JSONFormatter{
		TimestampFormat: "2006-01-02 15:04:05",
	})
	logrus.SetReportCaller(false)
}

// 写入程序员自定义的日志
func Write(msg string, filename string) {
	setOutPutFile(logrus.InfoLevel, filename)
	logrus.Info(msg)
}

// Debug 级别日志
func Debug(fields logrus.Fields, args ...interface{}) {
	setOutPutFile(logrus.DebugLevel, "debug")
	logrus.WithFields(fields).Debug(args)
}

// Info 级别日志
func Info(fields logrus.Fields, args ...interface{}) {
	setOutPutFile(logrus.InfoLevel, "info")
	logrus.WithFields(fields).Info(args)
}

// Warn 级别日志
func Warn(fields logrus.Fields, args ...interface{}) {
	setOutPutFile(logrus.WarnLevel, "warn")
	logrus.WithFields(fields).Warn(args)
}

// Fatal 级别日志
func Fatal(fields logrus.Fields, args ...interface{}) {
	setOutPutFile(logrus.FatalLevel, "fatal")
	logrus.WithFields(fields).Fatal(args)
}

// Error 级别日志
func Error(fields logrus.Fields, args ...interface{}) {
	setOutPutFile(logrus.ErrorLevel, "error")
	logrus.WithFields(fields).Error(args)
}

// Panic 级别日志
func Panic(fields logrus.Fields, args ...interface{}) {
	setOutPutFile(logrus.PanicLevel, "panic")
	logrus.WithFields(fields).Panic(args)
}

// Trace 级别日志
func Trace(fields logrus.Fields, args ...interface{}) {
	setOutPutFile(logrus.TraceLevel, "trace")
	logrus.WithFields(fields).Trace(args)
}

// 设置日志输出文件 在各个函数方法中调用
func setOutPutFile(level logrus.Level, logName string) {
	// 创建日志目录
	logDir := "./runtime/log"
	if _, err := os.Stat(logDir); os.IsNotExist(err) {
		err = os.MkdirAll(logDir, 0777)
		if err != nil {
			panic(fmt.Errorf("create log dir '%s' error: %s", logDir, err))
		}
	}

	// 获取当前日期字符串
	timeStr := time.Now().Format("2006-01-02")
	fileName := filepath.Join(logDir, logName+"_"+timeStr+".log")

	// 打开日志文件,如果不存在则创建
	var err error
	os.Stderr, err = os.OpenFile(fileName, os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0644)
	if err != nil {
		fmt.Println("open log file err:", err)
	}

	// 设置日志输出到文件
	logrus.SetOutput(os.Stderr)
	logrus.SetLevel(level)
	return
}

// 创建了success开头的文件 在logger中以中间件的形式调用
func LoggerToFile() gin.LoggerConfig {
	logDir := "./runtime/log"
	if _, err := os.Stat(logDir); os.IsNotExist(err) {
		err = os.MkdirAll(logDir, 0777)
		if err != nil {
			panic(fmt.Errorf("create log dir '%s' error: %s", logDir, err))
		}
	}

	// 获取当前日期字符串
	timeStr := time.Now().Format("2006-01-02")
	fileName := path.Join(logDir, "success_"+timeStr+".log")

	os.Stdout, _ = os.OpenFile(fileName, os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0644)

	var conf = gin.LoggerConfig{
		Formatter: func(param gin.LogFormatterParams) string {
			return fmt.Sprintf("%s - [%s] \"%s %s %s %d %s \"%s\" %s\"\n",
				param.TimeStamp.Format(time.RFC1123),
				param.ClientIP,
				param.Method,
				param.Path,
				param.Request.Proto,
				param.StatusCode,
				param.Latency,
				param.Request.UserAgent(),
				param.ErrorMessage,
			)
		},
		Output: io.MultiWriter(os.Stdout, os.Stderr),
	}
	return conf
}

// 将报错放在报文中返回回来 在logger中以中间件的形式调用
func Recover(c *gin.Context) {
	defer func() {
		if err := recover(); err != nil {
			if _, errDir := os.Stat("./runtime/log"); os.IsNotExist(errDir) {
				errDir := os.MkdirAll("./runtime/log", 0777)
				if errDir != nil {
					panic(fmt.Errorf("create log dir '%s' error: %s", "./runtime/log", err))
				}
			}
			timeStr := time.Now().Format("2006-01-02")
			//文件名
			fileName := path.Join("./runtime/log", timeStr+".log")

			f, errFile := os.OpenFile(fileName, os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0644)
			if errFile != nil {
				fmt.Println(errFile)
			}
			timeFileStr := time.Now().Format("2006-01-02 15:04:05")
			//写入信息
			f.WriteString("panic error tome:" + timeFileStr + "\n")
			f.WriteString(fmt.Sprintf("%v", err) + "\n")
			f.WriteString("stacktrace from panic" + string(debug.Stack()) + "\n")
			f.Close()
			c.JSON(200, gin.H{
				"code": 500,
				"msg":  fmt.Sprintf("%v", err),
			})
			//终止后续接口调用,不加入recover到异常之后,还会继续执行接口中的后续代码
			c.Abort()
		}
	}()

	c.Next()
}

可以结合注释简单了解以下,然后cv使用即可

调用方式

对于前两种:项目的请求日志(LoggerToFile)和 程序出现错误日志(Recover)而言,调用的方式是在路由(router.go)的函数中以中间件的方式调用

//日志
	r.Use(gin.LoggerWithConfig(logger.LoggerToFile()))
	r.Use(logger.Recover)

而对于 程序员开发的时候自己保存的日志(setOutPutFile),则是以方法的形式在想要记录的函数(方法)中调用logger.Write("日志信息", "user")

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值