1 概述
Gin是Golang的一个web框架, 封装比较优雅, API友好, 源码注释比较明确, 具有快速灵活, 容错方便等特点.对于想我这样的Go语言初学者, 选用Gin来构建Web服务是一个不错的选择.
构建一个简单的Web项目,除了Gin框架还用到的库有
- viper: 读取配置文件,将配置文件的内容映射到结构体上
- zap: 非常快的, 结构化的, 分级别的日志库
- lumberjack: 处理日志文件轮转, 日志打到一定大小时进行轮转,避免日志文件过大
- gorm: 对象关系映射(ORM)框架
- swagger: 接口文档
创建Go的项目,目录如图所示:
引入gin:
go get -u github.com/gin-gonic/gin
2 解析配置文件
创建配置文件初始化函数,使用viper解析配置文件, 将yaml文件的内容映射到结构体中,使用哪个文件可以在启动命令中指定。项目启动的时候在主函数调用此函数初始化配置文件。
2.1 引入viper
go get github.com/spf13/viper
2.2 创建配置文件
name: "shopping"
mode: "dev"
ip: "127.0.0.1"
port: 8080
version: "v0.0.1"
# 停机超时时间 单位秒
shutdown_overtime: 5
# 雪花算法配置
# 起始时间, 可以自定义开始时间
start_time: "2022-06-01"
#机器id, 多实例机器id不能相同
machine_id: 1
# 日志配置
log:
# 日志等级
level: "debug"
# 日志文件的位置
filename: "./log/shopping.log"
# 在进行切割之前,日志文件的最大大小(以MB为单位)
max_size: 200
# 保留旧文件的最大天数
max_age: 30
# 保留旧文件的最大个数
max_backups: 7
# mysql数据库配置
mysql:
host: "127.0.0.1"
port: 3306
user: user
password: psaaword
dbname: "go_shopping"
# 最大连接数
max_open_conns: 100
# 最大空闲数
max_idle_conns: 10
jwt:
secret_key: "abcde"
2.3 解析配置文件
package config
import (
"fmt"
"github.com/fsnotify/fsnotify"
"github.com/spf13/viper"
)
// Conf 定义全局变量
// new()函数 获取指定类型的指针, 有点像Java中的创建对象
var Conf = new(ServConfig)
type ServConfig struct {
Name string `mapstructure:"name"` // 服务名称
Mode string `mapstructure:"mode"` // 环境
Version string `mapstructure:"version"` // 版本
ShutdownOvertime int `mapstructure:"shutdown_overtime"` // 停机超时时间
IP string `mapstructure:"ip"`
Port string `mapstructure:"port"`
// 雪花算法配置
StartTime string `mapstructure:"start_time"`
MachineId int64 `mapstructure:"machine_id"`
// 日志配置
*LogConfig `mapstructure:"log"`
// MySQL配置
*MySQLConfig `mapstructure:"mysql"`
// JWT 配置信息
*JwtConfig `mapstructure:"jwt"`
}
// LogConfig 日志配置
type LogConfig struct {
// 日志级别
Level string `mapstructure:"level"`
// 志文件的位置
Filename string `mapstructure:"filename"`
// 在进行切割之前,日志文件的最大大小(以MB为单位)
MaxSize int `mapstructure:"max_size"`
// 保留旧文件的最大天数
MaxAge int `mapstructure:"max_age"`
// 保留旧文件的最大个数
MaxBackups int `mapstructure:"max_backups"`
}
// MySQLConfig MySQL配置信息
type MySQLConfig struct {
Host string `mapstructure:"host"`
User string `mapstructure:"user"`
Password string `mapstructure:"password"`
DB string `mapstructure:"dbname"`
Port int `mapstructure:"port"`
// 最大连接数
MaxOpenConns int `mapstructure:"max_open_conns"`
// 最大空闲连接数
MaxIdleConns int `mapstructure:"max_idle_conns"`
}
// JwtConfig Jwt 配置信息
type JwtConfig struct {
SecretKey string `mapstructure:"secret_key"`
}
// Init 整个服务配置文件初始化的方法 (程序启动的时候加载配置文件)
func Init(filPath string) (err error) {
// 直接指定配置文件路径(相对路径或者绝对路径)
viper.SetConfigFile(filPath)
// 读取配置文件信息
if err = viper.ReadInConfig(); err != nil {
return err
}
// 把配置文件中的信息反序列化到Conf全局变量中, 在其他地方用到配置文件中的属性的时候,可以使用全局变量中的属性
if err = viper.Unmarshal(Conf); err != nil {
return err
}
// 配置文件的监听
viper.WatchConfig()
// 配置文件发生变化之后,会执行下面钩子函数的监听代码
viper.OnConfigChange(func(in fsnotify.Event) {
fmt.Println("配置文件被修改了")
if err := viper.Unmarshal(Conf); err != nil {
fmt.Printf("viper.Unmarshal failed, err:%v\n", err)
}
})
return
}
3 初始化日志
go语言提供的默认的Go Lagger只提供了基本的日志级别,不支持INFO/DEBUG等多级别。缺乏日志格式化的能力,不能记录调用者的函数名和行号,格式化日期和时间格式。不提供日志切割的能力。
Zap提供了两种类型的日志记录器—Sugared Logger和Logger。
在性能很好但不是很关键的上下文中,使用SugaredLogger。它比其他结构化日志记录包快4-10倍,并且支持结构化和printf风格的日志记录。
在每一微秒和每一次内存分配都很重要的上下文中,使用Logger。它甚至比SugaredLogger更快,内存分配次数也更少,但它只支持强类型的结构化日志记录。
创建初始化日志的函数对zap及逆行初始化,在项目主函数中进行调用。
3.1 引入zap&lumberjack
go get go.uber.org/zap
go get go.uber.org/zap/zapcore
go get gopkg.in/natefinch/lumberjack.v2
3.2 初始化日志配置
package logger
import (
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
"gopkg.in/natefinch/lumberjack.v2"
"os"
"shopping/config"
)
var Log *zap.Logger
// zap日志的三要素
// 1. encdor编码 2.输出位置 3. 日志级别
// Init 初始化日志
// 如果需要根据环境打印不同的日志, 也可以根据配置文件中的mode 来作为判断条件, 在这个函数的入参里面增加一个mode, 将配置文件里的数据传进来
func Init(cfg *config.LogConfig) (err error) {
writerSyncer := getLogWriter(cfg.Filename, cfg.MaxSize, cfg.MaxBackups, cfg.MaxAge)
encoder := getEncoder()
var l = new(zapcore.Level)
if err = l.UnmarshalText([]byte(cfg.Level)); err != nil {
return err
}
core := zapcore.NewTee(
zapcore.NewCore(encoder, writerSyncer, l),
zapcore.NewCore(encoder, zapcore.Lock(os.Stdout), l),
)
// 初始化一个全局对象, 并添加调用栈信息
Log = zap.New(core, zap.AddCaller())
zap.ReplaceGlobals(Log) // 替换zap包全局的logger
zap.L().Info("日志初始化成功")
return
}
// getLogWriter 指定日志将写到哪里去
// 使用第三方库做日志切分,记录日志的时候同时做一个日志的轮转和切分
func getLogWriter(filename string, maxSize, maxBackup, maxAge int) zapcore.WriteSyncer {
lumberJackLogger := &lumberjack.Logger{
Filename: filename,
MaxSize: maxSize,
MaxBackups: maxBackup,
MaxAge: maxAge,
}
return zapcore.AddSync(lumberJackLogger)
}
// getEncoder 编码器(定义如何写入日志)
func getEncoder() zapcore.Encoder {
encoderConfig := zap.NewProductionEncoderConfig()
// 修改时间格式
encoderConfig.EncodeTime = zapcore.ISO8601TimeEncoder
encoderConfig.TimeKey = "time"
encoderConfig.EncodeLevel = zapcore.CapitalLevelEncoder
encoderConfig.EncodeDuration = zapcore.SecondsDurationEncoder
encoderConfig.EncodeCaller = zapcore.ShortCallerEncoder
return zapcore.NewConsoleEncoder(encoderConfig)
}
4 初始化Gorm创建数据库链接
4.1 引入gorm
go get -u gorm.io/gorm
go get -u gorm.io/driver/mysql
4.2 初始化数据库连接
package mysql
import (
"context"
"fmt"
"go.uber.org/zap"
"gorm.io/driver/mysql"
"gorm.io/gorm"
gormLogger "gorm.io/gorm/logger"
"shopping/config"
"shopping/logger"
"time"
)
// Db 全局的Db对象
var Db *gorm.DB
// Init 初始化数据库
func Init(cfg *config.MySQLConfig) (err error) {
// 参考 https://github.com/go-sql-driver/mysql#dsn-data-source-name 获取详情
// parseTime=True可以将数据库中的时间类型映射成Go语言中的时间类型 loc=Local 地区
dsn := fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?charset=utf8mb4&parseTime=True&loc=Local", cfg.User, cfg.Password, cfg.Host, cfg.Port, cfg.DB)
Db, err = gorm.Open(mysql.Open(dsn), &gorm.Config{
Logger: loggerCallback{logger.Log},
})
if err != nil {
return err
}
// 额外的连接配置
// 使用sqlDB设置连接池
sqlDB, err := Db.DB() // database/sql.DB
if err != nil {
return err
}
// 以下配置要配合 my.conf 进行配置
// SetMaxIdleConns 设置空闲连接池中连接的最大数量
sqlDB.SetMaxOpenConns(cfg.MaxIdleConns)
// SetMaxOpenConns 设置打开数据库连接的最大数量。
sqlDB.SetMaxOpenConns(cfg.MaxOpenConns)
// SetConnMaxLifetime 设置了连接可复用的最大时间。
sqlDB.SetConnMaxLifetime(time.Hour)
logger.Log.Info("数据库初始化成功")
return
}
// loggerCallback 实现了 GORM 的回调接口
type loggerCallback struct {
logger *zap.Logger
}
// LogMode 使用 Zap 打印 SQL 语句
func (l loggerCallback) LogMode(level gormLogger.LogLevel) gormLogger.Interface {
return l
}
// Info 使用 Zap 打印 SQL 语句
func (l loggerCallback) Info(ctx context.Context, msg string, data ...interface{}) {
l.logger.Info(fmt.Sprintf(msg, data...))
}
// Warn 使用 Zap 打印 SQL 语句
func (l loggerCallback) Warn(ctx context.Context, msg string, data ...interface{}) {
l.logger.Warn(fmt.Sprintf(msg, data...))
}
// Error 使用 Zap 打印 SQL 语句
func (l loggerCallback) Error(ctx context.Context, msg string, data ...interface{}) {
l.logger.Error(fmt.Sprintf(msg, data...))
}
// Trace 使用 Zap 打印 SQL 语句
func (l loggerCallback) Trace(ctx context.Context, begin time.Time, fc func() (string, int64), err error) {
if err != nil {
sql, rows := fc()
l.logger.Error("gorm trace error", zap.Error(err), zap.String("sql", sql), zap.Int64("rows", rows))
} else {
sql, rows := fc()
l.logger.Debug("gorm trace", zap.String("sql", sql), zap.Int64("rows", rows), zap.Duration("elapsed", time.Since(begin)))
}
}
4.3 初始话雪花算法
4.3.1 引入雪花算法
go get github.com/bwmarrin/snowflake
4.3.2 初始化
package snowflake
import (
"errors"
"time"
sf "github.com/bwmarrin/snowflake"
)
const (
_dafaultStartTime = "2020-12-31" // 默认开始时间
)
var node *sf.Node
// Init 雪花算法组件初始化,正常应该把雪花算法当成一个独立的服务部署
// startTime 开始时间
// machineID 机器id
func Init(startTime string, machineID int64) (err error) {
if machineID < 0 {
return errors.New("snowflake need machineID")
}
if len(startTime) == 0 {
startTime = _dafaultStartTime
}
var st time.Time
st, err = time.Parse("2006-01-02", startTime)
if err != nil {
return
}
sf.Epoch = st.UnixNano() / 1000000 // 时间戳的开始时间,默认从1970年开始计算
node, err = sf.NewNode(machineID) // 机器编号,最多1024
return
}
// GenID 生成18位雪花算法id
func GenID() int64 {
return node.Generate().Int64()
}
// GenIDStr 生成18位雪花算法id
func GenIDStr() string {
return node.Generate().String()
}
5 创建接口
以查询接口举例
package category
import (
"fmt"
"github.com/gin-gonic/gin"
"net/http"
"shopping/model"
"shopping/service"
"shopping/utils/api_helper"
"shopping/utils/pagination"
"shopping/utils/snowflake"
"time"
)
// Controller 分类控制器
type Controller struct {
categoryService *service.CategoryService
}
// NewCategoryController 实例化控制器
func NewCategoryController(s *service.CategoryService) *Controller {
return &Controller{
categoryService: s,
}
}
// GetCategories 查询分类列表
// @Summary 获得分类列表
// @Tags Category
// @Produce json
// @param page query int false "Page number"
// @param pageSize query int false "Page size"
// @Success 200 {object} pagination.Pages
// @Router /category [get]
func (c *Controller) GetCategories(g *gin.Context) {
page := pagination.NewFormGinRequest(g, -1)
page = c.categoryService.GetALl(page)
g.JSON(http.StatusOK, page)
}
// CategoryService 商品分类Service
type CategoryService struct {
categoryDao *mysql.CategoryDao
}
// NewCategoryService 实例化商品分类service
func NewCategoryService(dao *mysql.CategoryDao) *CategoryService {
return &CategoryService{
categoryDao: dao,
}
}
// GetALl 获得分页商品分类
func (service *CategoryService) GetALl(page *pagination.Pages) *pagination.Pages {
categories, count, err := service.categoryDao.GetAll(page.Page, page.PageSize)
if err != nil {
}
page.Items = categories
page.TotalCount = count
return page
}
// CategoryDao 商品分类Dao
type CategoryDao struct {
db *gorm.DB
}
// NewCategoryDao 创建NewCategoryDao
func NewCategoryDao(db *gorm.DB) *CategoryDao {
return &CategoryDao{
db: db,
}
}
// GetAll 获得分页商品分类
func (dao *CategoryDao) GetAll(pageIndex, pageSize int) ([]model.Category, int, error) {
var categories []model.Category
var count int64
err := dao.db.Offset((pageIndex - 1) * pageSize).Limit(pageSize).Find(&categories).Count(&count).Error
return categories, int(count), err
}
6 Swagger
6.1 引入swagger
go get -u github.com/swaggo/swag/cmd/swag
go get github.com/swaggo/gin-swagger
go get github.com/swaggo/gin-swagger/swaggerFiles
6.2 在接口和主函数中添加注释
// GetCategories 查询分类列表
// @receiver Controller
// @Summary 获得分类列表
// @Tags Category
// @Produce json
// @param page query int false "Page number"
// @param pageSize query int false "Page size"
// @Success 200 {object} pagination.Pages
// @Router /category [get]
func (c *Controller) GetCategories(g *gin.Context) {
page := pagination.NewFormGinRequest(g, -1)
page = c.categoryService.GetALl(page)
g.JSON(http.StatusOK, page)
}
// CreateCategory
// @Description 创建分类
// @receiver Controller
// @Summary 根据给定的参数创建分类
// @Tags Category
// @Accept json
// @Produce json
// @param Authorization header string true "Authorization header"
// @param CreateCategoryRequest body CreateCategoryRequest true "category information"
// @Success 200 {object} api_helper.Response
// @Failure 400 {object} api_helper.ErrorResponse
// @Router /category [post]
func (c *Controller) CreateCategory(g *gin.Context) {
var req CreateCategoryRequest
if err := g.ShouldBind(&req); err != nil {
// 解析参数异常
api_helper.HandleError(g, err)
return
}
newCategory := model.NewCategory(req.Name, req.Desc)
newCategory.CreateTime = time.Now()
newCategory.Id = snowflake.GenID()
err := c.categoryService.Create(newCategory)
if err != nil {
api_helper.HandleError(g, err)
return
}
g.JSON(http.StatusCreated, api_helper.Response{
Message: "category created",
})
}
package main
import (
"shopping/bootstrap"
_ "shopping/docs"
)
// @title 电商demo
// @description go语言学习项目
// @version 1.0
// @contact.name liu
// @contact.url http://.....
// @host localhost:8080
// @BasePath
func main() {
// 运行项目
bootstrap.Run()
}
6.3 安装swap
go install github.com/swaggo/swag/cmd/swag
使用swag.init生成swagger文件
7 编写主函数
在主函数中调用以上所有初始化函数,注册自定义中间件(日志和异常保护),注册控制器,启动服务,添加监听停机处理。
package main
import (
"shopping/bootstrap"
_ "shopping/docs"
)
// @title 电商demo
// @description go语言学习项目
// @version 1.0
// @contact.name liu
// @contact.url http://.....
// @host localhost:8080
// @BasePath
func main() {
// 运行项目
bootstrap.Run()
}
package bootstrap
import (
"context"
"flag"
"fmt"
"github.com/gin-gonic/gin"
swaggerFiles "github.com/swaggo/files"
ginSwagger "github.com/swaggo/gin-swagger"
"go.uber.org/zap"
"net/http"
"os"
"os/signal"
"shopping/config"
"shopping/controller"
"shopping/dao/mysql"
"shopping/logger"
"shopping/utils/middleware"
"shopping/utils/snowflake"
"syscall"
"time"
)
// Run 运行项目
func Run() {
// 初始化资源
initBeforeRun()
// 启动服务
srv := runGinServer()
// 监听关闭服务
shutdown(srv)
}
// initBeforeRun 初始化各种资源
func initBeforeRun() {
// 加载配置文件
initConfig()
// 初始化日志
initLogger()
// 初始化MYSQL
initMySQL()
// 初始化雪花算法
initSnowflake()
}
// initConfig 加载配置文件
func initConfig() {
// 配置文件路径
var cfn string
// 用来解析命令行中的命令, 可以在命令行中执行配置, 命令行中没有指定配置文件路径, 使用项目中的配置文件
// 无论是直接运行, 还是生成可执行文件运行, 都可以在命令后面追加 -conf="./conf/config-dev.yaml" 指定配置文件
// go run main.go -conf="./conf/config-uat.yaml"
flag.StringVar(&cfn, "conf", "./conf/config-dev.yaml", "指定配置文件路径")
// 解析命令行标志
flag.Parse()
// 加载配置文件
err := config.Init(cfn)
if err != nil {
panic(err)
}
}
// initLogger 初始化日志
func initLogger() {
err := logger.Init(config.Conf.LogConfig)
if err != nil {
panic(err)
}
}
// initMySQL 初始化MySQL
func initMySQL() {
err := mysql.Init(config.Conf.MySQLConfig)
if err != nil {
panic(err)
}
}
// initSnowflake 初始化雪花算法
func initSnowflake() {
err := snowflake.Init(config.Conf.StartTime, config.Conf.MachineId)
if err != nil {
panic(err)
}
}
// runGinServer 启动gin服务
func runGinServer() *http.Server {
// 获取gin引擎
r := gin.New()
// 注册中间件
registerMiddlewares(r)
// 注册控制器
controller.RegisterHandlers(r)
// 注册swagger
//url := ginSwagger.URL("/swagger/docs.json") // The url pointing to API definition
r.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler))
// 创建服务器
srv := &http.Server{
Addr: fmt.Sprintf("%s:%s", config.Conf.IP, config.Conf.Port),
Handler: r,
}
go func() {
// 启动HTTP服务器
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
logger.Log.Fatal("监听并启动服务失败: \n", zap.Error(err))
}
}()
logger.Log.Info("服务已经启动",
zap.String("url", fmt.Sprintf("http://localhost:%s", config.Conf.Port)),
zap.String("swaggerUrl", fmt.Sprintf("http://localhost:%s/swagger/index.html", config.Conf.Port)))
return srv
}
// shutdown 听信号, 执行关机操作
// param srv 需要关闭的Http Server实例
func shutdown(srv *http.Server) {
// 定义一个触发退出的信号, 通道的元素是os.Signal, 是操作系统的信号
quit := make(chan os.Signal)
// 操作发出syscall.SIGTERM和syscall.SIGINT这两个信号的时候,会把它通知到自定义的quite中
// kill (没有参数) 默认是 syscall.SIGTERM
// kill -2 是 syscall.SIGINT
// kill -9 是 syscall.
signal.Notify(quit, syscall.SIGTERM, syscall.SIGINT)
// 启动之后 这里会阻塞, 等到退出的时候(通道里面有值)才会往下执行
<-quit
logger.Log.Info("开始关闭服务...")
// 创建context 并设置超时时间, 确保服务关闭的操作在指定时间内完成
ctx, cancel := context.WithTimeout(context.Background(), time.Second*time.Duration(config.Conf.ShutdownOvertime))
defer cancel()
// 其他操作, 例如在注册中心中注销服务
// 调用Http实例的Shutdown方法 关闭服务器
if err := srv.Shutdown(ctx); err != nil {
logger.Log.Fatal("服务关闭错误: ", zap.Error(err))
}
logger.Log.Info("关闭服务完成...")
}
// registerMiddlewares 注册中间件
func registerMiddlewares(r *gin.Engine) {
// 打印日志
r.Use(middleware.GinLogger(logger.Log))
// 异常保护
r.Use(middleware.GinRecovery(logger.Log, true))
}
// Package middleware
// Author liuzhiyong
// Date 2023/11/16
// Description 基础的中间件 做日志打印 和 recover
package middleware
import (
"bytes"
"fmt"
"github.com/gin-gonic/gin"
"go.uber.org/zap"
"net"
"net/http"
"net/http/httputil"
"os"
"runtime/debug"
"strings"
"time"
)
// CustomResponseWriter 自定义的ResponseWriter 用于获取响应数据
type CustomResponseWriter struct {
gin.ResponseWriter
body *bytes.Buffer // 响应体缓存
}
// Write
func (w CustomResponseWriter) Write(b []byte) (int, error) {
w.body.Write(b)
return w.ResponseWriter.Write(b)
}
// WriteString
func (w CustomResponseWriter) WriteString(s string) (int, error) {
w.body.WriteString(s)
return w.ResponseWriter.WriteString(s)
}
// GinLogger 日志中间件
func GinLogger(logger *zap.Logger) gin.HandlerFunc {
return func(c *gin.Context) {
start := time.Now()
path := c.Request.URL.Path
query := c.Request.URL.RawQuery
logger.Info(fmt.Sprintf("请求开始: %s", path),
zap.String("method", c.Request.Method),
zap.String("path", path),
zap.String("query", query),
zap.String("ip", c.ClientIP()),
zap.String("user-agent", c.Request.UserAgent()),
)
// 替换运来的Writer
blw := &CustomResponseWriter{body: bytes.NewBufferString(""), ResponseWriter: c.Writer}
c.Writer = blw
c.Next()
logger.Info(fmt.Sprintf("请求结束: %s", path),
zap.Int("status", c.Writer.Status()),
zap.String("path", path),
zap.String("response", blw.body.String()),
zap.String("errors", c.Errors.ByType(gin.ErrorTypePrivate).String()),
zap.Int64("cost", time.Since(start).Microseconds()),
)
}
}
// GinRecovery recover掉项目可能出现的panic
// param: logger
// param: stack 是否打印堆栈信息
func GinRecovery(logger *zap.Logger, stack bool) gin.HandlerFunc {
return func(c *gin.Context) {
defer func() {
// 执行recover() 捕获异常
if err := recover(); err != nil {
//检查是否有断开的连接,因为这并不是一个真正需要进行紧急堆栈跟踪的条件。
var brokenPipe bool
// 如果异常时net.OpError类型 ==> 转为 对应类型 err.() 类型断言的写法, 判断类型, 转换类型
if ne, ok := err.(*net.OpError); ok {
if se, ok := ne.Err.(*os.SyscallError); ok {
if strings.Contains(strings.ToLower(se.Error()), "broken pipe") ||
strings.Contains(strings.ToLower(se.Error()), "connection reset by peer") {
brokenPipe = true
}
}
}
// 将http请求转换成字节切面, 第二个布尔类型的参数表示是否包括请求体, 为false的话表示只包括请求头
httpRequest, _ := httputil.DumpRequest(c.Request, false)
if brokenPipe {
logger.Error(c.Request.URL.Path,
zap.Any("error", err),
zap.String("request", string(httpRequest)),
)
// If the connection is dead, we can't write a status to it.
c.Error(err.(error)) // nolint: errcheck
c.Abort()
return
}
if stack {
logger.Error("[Recovery from panic]",
zap.Any("error", err),
zap.String("request", string(httpRequest)),
zap.String("stack", string(debug.Stack())),
)
} else {
logger.Error("[Recovery from panic]",
zap.Any("error", err),
zap.String("request", string(httpRequest)),
)
}
c.AbortWithStatus(http.StatusInternalServerError)
// c.JSON(200, gin.H{"code": 1, "msg": "出错啦"})
// return
}
}()
c.Next() // 调用后续的逻辑
}
}