中间件与生态

日志系统

引入

现在已经2026年了,现在的日志不只是简单的文本文件,而是可观测性的基石,我们当然不能用Println去打印日志了,我们需要一个更加结构化,更加高效的日志系统。

基本介绍

时至今日,Uber-go/zap依旧是高性能日志的常用库,Lumbjack是文件切割的标准组件。

我们再终端执行

 go get -u go.uber.org/zap
 go get -u github.com/natefinch/lumberjack

来引入

部署

更新配置

在配置文件中加入:

 log:
  level :"info"
  root_path:"./logs"
  filename:"server.log"
  max_size:10
  max_age:30
  compress:true #是否开启gzip压缩
  format:"console" #输出格式在生产环境为json
  • 日志级别:debug < info < warn < error
  • 生产环境(Prod)通常要求日志格式为 JSON,以便被 ELK 或 Datadog 等日志平台自动抓取分析。

完善模型

我们需要结构体来接受YAML的配置

 package model
 ​
 type Config struct {
  Server Server `mapstructure:"server"`
  Mysql Mysql `mapstructure:"mysql"`
  Log   Log   `mapstructure:"log"`
 }
 ​
 type Server struct {
  Port int `mapstructure:"port"`
 }
 ​
 type Mysql struct {
  Path     string `mapstructure:"path"`
  Config   string `mapstructure:"config"`
  DbName   string `mapstructure:"db-name"`
  User     string `mapstructure:"user"`
  Password string `mapstructure:"password"`
 }
 ​
 type Log struct {
  Level   string `mapstructure:"level"`
  RootPath string `mapstructure:"root_path"`
  Filename string `mapstructure:"filename"`
  MaxSize int   `mapstructure:"max_size"`
  MaxAge   int   `mapstructure:"max_age"`
  Compress bool   `mapstructure:"compress"`
  Format   string `mapstructure:"format"`
 }

增加全局变量

 package global
 ​
 import (
  "my_gin_app/internal/model"
  "go.uber.org/zap"
  "gorm.io/gorm"
 )
 ​
 var (
  Config model.Config
  DB     *gorm.DB
  Logger *zap.Logger
 )

初始化

我们要实现下面的功能:

  1. 多路输出,要在控制台输出也要写入文件
  2. 文件切割,文件打了自动切分,不要一个文件写的特别长然后炸了
  3. 上线的时候输出json
 package initialize
 ​
 import (
  "fmt"
  "go.uber.org/zap"
  "go.uber.org/zap/zapcore"
  "gopkg.in/natefinch/lumberjack.v2"
  "my-gin-app/internal/global"
  "os"
 )
 ​
 func InitLogger() {
  cfg := global.Config.Log
  /*切割*/
  hook := &lumberjack.Logger{
  Filename:   fmt.Sprintf("%s%s", cfg.RootPath, cfg.Filename),
  MaxSize:    cfg.MaxSize,
  MaxBackups: 3,
  MaxAge:     cfg.MaxAge,
  Compress:   cfg.Compress,
  }
  /*时间格式*/
  encoderConfig := zap.NewProductionEncoderConfig()
  encoderConfig.EncodeTime = zapcore.TimeEncoderOfLayout("2006-01-02 15:04:05")
  encoderConfig.EncodeLevel = zapcore.CapitalLevelEncoder
 ​
  /*判断输出格式*/
  var encoder zapcore.Encoder
  if cfg.Format == "json" {
  encoder = zapcore.NewJSONEncoder(encoderConfig)
  } else {
  encoder = zapcore.NewConsoleEncoder(encoderConfig)
  }
  /*设置日志级别*/
  level := zap.InfoLevel
  if cfg.Level == "debug" {
  level = zap.DebugLevel
  }
  /*核心——怎么编码,写到哪,什么级别*/
  core := zapcore.NewCore(
  encoder,
  zapcore.NewMultiWriteSyncer(
  zapcore.AddSync(os.Stdout),
  zapcore.AddSync(hook),
  ),
  level,
  )
  global.Logger = zap.New(core, zap.AddCaller())
  /*替换全局标准库log*/
  zap.ReplaceGlobals(global.Logger)
 }
 ​

启动

 package main
 ​
 import (
     "fmt"
     "my-gin-app/internal/global"
     "my-gin-app/internal/initialize"
     "my-gin-app/internal/router"
 ​
     "go.uber.org/zap"
 )
 ​
 func main() {
     initialize.InitConfig()
     initialize.InitLogger()
     global.Logger.Info("启动")
     // 1. 初始化配置和数据库
     initialize.InitConfig()
     initialize.InitDB()
     global.Logger.Info("数据库连接成功")
     // 2. 加载路由配置
     r := router.SetupRouter()
     port := global.Config.Server.Port
     global.Logger.Info("服务运行ing",zap.Int("端口",port))
     // 3. 启动服务
     r.Run(fmt.Sprintf(":%d", global.Config.Server.Port))
 }

然后我们要把 fmt.Println 替换为 global.Logger.Infoglobal.Logger.Error

在service添加日志

 package service
 ​
 import (
  "errors"
  "my-gin-app/internal/dao"
  "my-gin-app/internal/global"
  "my-gin-app/internal/model"
 ​
  "go.uber.org/zap"
  "gorm.io/gorm"
 )
 ​
 type UserService struct{}
 ​
 var User = new(UserService)
 ​
 /*创建新用户业务逻辑*/
 func (s *UserService) CreateUser(user *model.User) error {
  global.Logger.Info("创建用户:", zap.String("name", user.Name))
  /*查一下是否存在*/
  existUser, err := dao.User.GetByName(user.Name)
  if err == nil && existUser.ID > 0 {
  global.Logger.Warn("用户创建失败:用户名已占用", zap.String("name", user.Name))
  return errors.New("用户名已存在")
  }
  if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
  global.Logger.Error("用户查重时数据库出错", zap.Error(err))
  return err
  }
  if user.Age < 0 {
  global.Logger.Warn("用户创建失败:年龄不合法", zap.Int("age", user.Age))
  return errors.New("年龄不能为负数")
  }
  if err := dao.User.Create(user); err != nil {
  // 创建失败
  global.Logger.Error("保存用户到数据库失败", zap.Error(err))
  return err
  }
 ​
  // 成功
  global.Logger.Info("用户创建成功", zap.Int("new_id", int(user.ID)))
  return nil
 }
 ​
 func (s *UserService) DeleteUser(id int) error {
  if id == 1 {
  return errors.New("无法删除")
  }
  return dao.User.Delete(id)
 }

JWT中间件鉴权

基本介绍

JSON Web Token (JWT)是一个开放标准(RFC 7519),它定义了一种紧凑的、自包含的方式,用于作为JSON对象在各方之间安全地传输信息。该信息可以被验证和信任,因为它是数字签名的。

部署

安装JWT库

 go get -u github.com/golang-jwt/jwt/v5

更新配置

 jwt:
  signing_key: "gin_rank_secret_key_2026" # 生产环境用复杂的随机字符串
  expires_time: "24h"                     # Token 有效期

完善模型

 package model
 ​
 type Config struct {
  Server Server `mapstructure:server"`
  Mysql  Mysql  `mapstructure:mysql`
  Log    Log    `mapstructure:"log"`
  JWT    JWT    `mapstructure:"jwt"`
 }
 type Server struct {
  Port int `mapstructure:"port"`
 }
 type Mysql struct {
  Path     string `mapstructure:"path"`
  Config   string `mapstructure:"config"`
  DbName   string `mapstructure:"db-name"`
  User     string `mapstructure:"user"`
  Password string `mapstructure:"password"`
 }
 ​
 type Log struct {
  Level    string `mapstructure:"level"`
  RootPath string `mapstructure:"root_path"`
  Filename string `mapstructure:"filename"`
  MaxSize  int    `mapstructure:"max_size"`
  MaxAge   int    `mapstructure:"max_age"`
  Compress bool   `mapstructure:"compress"`
  Format   string `mapstructure:"format"`
 }
 type JWT struct {
  SigningKey  string `mapstructure:"signing_key"`
  ExpiresTime string `mapstructure:"expires_time"`
 }
 ​

编写生成和解析工具

新建文件:internal/utils/jwt.go

 package utils
 ​
 import (
  "errors"
  "my-gin-app/internal/global"
  "time"
 ​
  "github.com/golang-jwt/jwt/v5"
 )
 ​
 type MycustomClaims struct {
  UserId   int    `json:"user_id"`
  Username string `json:"username"`
  jwt.RegisteredClaims
 }
 ​
 func GenereateToken(userID int, username string) (string, error) {
  jwtCfg := global.Config.JWT
  duration, err := time.ParseDuration(jwtCfg.ExpiresTime)
  if err != nil {
  duration = 24 * time.Hour
  }
  claims := MycustomClaims{
  UserId:   userID,
  Username: username,
  RegisteredClaims: jwt.RegisteredClaims{
  ExpiresAt: jwt.NewNumericDate(time.Now().Add(duration)),
  Issuer:    "gin_rank_system", /*签发人*/
  },
  }
  token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) /*加密算法和数据*/
 ​
  return token.SignedString([]byte(jwtCfg.SigningKey))
  /*注意必须转成切片*/
 }
 ​
 /*校验*/
 func ParseToken(tokenStr string) (*MycustomClaims, error) {
  cfg := global.Config.JWT
  claims := &MycustomClaims{}
  token, err := jwt.ParseWithClaims(tokenStr, claims, func(token *jwt.Token) (any, error) {
  if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
  return nil, errors.New("unexpected signing method")
  }
  return []byte(cfg.SigningKey), nil
  })
  if err != nil{
  return nil,err
  }
  if !token.Valid{
  return nil, errors.New("token is invalid")
  }
  return claims ,nil
 }
 ​

中间件

internal/service/user.go

 package middleware
 ​
 import (
  "my-gin-app/internal/utils"
  "net/http"
  "strings"
 ​
  "github.com/gin-gonic/gin"
 )
 ​
 func JWTAuth() gin.HandlerFunc {
  return func(c *gin.Context) {
  authHeader := c.Request.Header.Get("Authorization")
  if authHeader == "" {
  c.JSON(http.StatusUnauthorized, gin.H{"error": "未登录"})
  c.Abort()
  return
  }
  /*格式校验*/
  parts := strings.SplitN(authHeader, " ", 2)
  if !(len(parts) == 2 && parts[0] == "Bearer") {
  c.JSON(http.StatusUnauthorized, gin.H{"error:": "格式错误"})
  c.Abort()
  return
  }
  /*解析token*/
  claims, err := utils.ParseToken(parts[1])
  if err != nil {
  c.JSON(http.StatusUnauthorized, gin.H{"error": "Token 无效或已过期"})
  c.Abort()
  return
  }
 ​
  /*存入上下文 */
  c.Set("userID", claims.UserId)
  c.Set("username", claims.Username)
 ​
  c.Next() // 放行
  }
 }
 ​

路由配置

 package router
 ​
 import (
  "my-gin-app/internal/controller"
  "my-gin-app/internal/middleware"
 ​
  "github.com/gin-gonic/gin"
 )
 ​
 func SetupRouter() *gin.Engine {
  r := gin.Default()
  /*公开接口*/
  public := r.Group("/api/v1")
  {
  public.POST("/login", controller.Login)       // 登录接口
  public.POST("/register", controller.Register) // 注册接口
  }
 ​
  /*受保护接口(需要 JWT)*/
  protected := r.Group("/api/v1")
  protected.Use(middleware.JWTAuth()) // 挂载中间件 🛡️
  {
  /*带了Token 才能访问下面*/
  protected.GET("/user/profile", controller.GetUserList)
  protected.PUT("/user", controller.UpdateUser)
  }
  return r
 }
 ​

业务层

internal/service/user.go

 func Login(c *gin.Context) {
  var input LoginInput
  if err := c.ShouldBindJSON(&input); err != nil {
  c.JSON(http.StatusBadRequest, gin.H{"error": "参数格式错误"})
  return
  }
  token, err := service.User.Login(input.Username, input.Password)
  if err != nil {
  c.JSON(http.StatusUnauthorized, gin.H{"error": err.Error()})
  return
  }
 ​
  c.JSON(http.StatusOK, gin.H{
  "msg":   "登录成功",
  "token": token,
  })
 }
 func Register(c *gin.Context) {
  C

设计哲学

为什么我们要这么麻烦引入 JWT?这背后体现了三个架构思想:

  1. 无状态:服务器什么都不记。所有信息都刻在 Token 里发给用户自己保管。 Go 程序可以随便启动 100 个实例,甚至重启服务器,只要密钥不变,用户依然处于登录状态。这是云原生微服务的基础。
  2. **以算力换空间 :解析 Token 需要进行加密运算(消耗 CPU),但不需要查数据库(节省 I/O),CPU 极其廉价,而数据库连接是瓶颈。JWT 让鉴权过程完全在内存中完成,速度极快。
  3. 信任链ParseWithClaims 的核心逻辑不是“解密”(JWT 的内容其实是 Base64 编码,谁都能看),而是“检验”。只要签名对得上,我们就绝对信任里面的 UserID 是真的,不需要再去数据库查一遍“这个用户是不是真的叫张三”。这体现了零信任架构中的凭证思维。

自动化接口文档(swagger)

每次写完接口去APIFOX里面一点一点填参数调试,好麻烦,Swagger可以根据你的代码注释把你的代码注释直接变成一个可交互的 UI 界面。

安装

 # 1. 安装命令行工具 (生成器)
 go install github.com/swaggo/swag/cmd/swag@latest
 ​
 # 2. 安装 Gin 的适配器
 go get -u github.com/swaggo/gin-swagger
 go get -u github.com/swaggo/files

配置

我们只需要在接口上方添加注释

 // @Summary 用户注册
 // @Description 创建一个新用户账号
 // @Tags 用户模块
 // @Accept json
 // @Produce json
 // @Param request body RegisterReq true "注册信息"
 // @Success 200 {object} Response "成功"
 // @Router /api/v1/register [post]
 func Register(c *gin.Context) {
  CreateUser(c)
 }

在项目根目录运行 swag init,它会生成一个 docs 文件夹。

我们在main中启动

 package main
 ​
 import (
  "fmt"
  swaggerFiles "github.com/swaggo/files"
  ginSwagger "github.com/swaggo/gin-swagger"
  "go.uber.org/zap"
  "my-gin-app/internal/global"
  "my-gin-app/internal/initialize"
  "my-gin-app/internal/router"
  _ "my_gin_app/docs"
 )
 ​
 func main() {
  initialize.InitConfig()
  initialize.InitLogger()
  global.Logger.Info("启动")
  // 1. 初始化配置和数据库
  initialize.InitConfig()
  initialize.InitDB()
  global.Logger.Info("数据库连接成功")
  // 2. 加载路由配置
  r := router.SetupRouter()
  port := global.Config.Server.Port
  global.Logger.Info("服务运行ing", zap.Int("端口", port))
  // 3. 启动服务
  r.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler))
  r.Run(fmt.Sprintf(":%d", global.Config.Server.Port))
 }

然后我们运行。

访问http://localhost:8080/swagger/index.html

image-20260207142853061

我们可以测试接口。

请求参数校验

Gin 框架内置了 validator 库,我们不需要用if去判断,只需要结构体上贴“标签”就能搞定一切。

 Username string `json:"username" binding:"required,min=3"`
标签含义例子
required必填,不能是零值binding:"required"
min / max字符串最小/最大长度binding:"min=5,max=10"
len字符串或数组的固定长度binding:"len=6"
eq必须等于某个值binding:"eq=admin"
oneof枚举,必须是列出的值之一binding:"oneof=red green blue"
email邮箱格式binding:"email"
urlURL链接格式binding:"url"
datetime日期格式binding:"datetime=2006-01-02"

上面是一个对照表

暂无评论

发送评论 编辑评论


				
上一篇
下一篇