Gin 基础操作

230 阅读3分钟

Gin 基础操作

参数解析与校验

解析请求参数、请求体、请求头,Validator 字段校验、自定义校验函数,标准 Rest 响应体等

package main

import (
    "errors"
    "log/slog"
    "net/http"
    "strings"

    "github.com/gin-gonic/gin"
    "github.com/gin-gonic/gin/binding"
    "github.com/go-playground/validator/v10"
)

var (
    err error
)

/*
    Entity
*/

// 标准Restful响应
type Restful[T any] struct {
    Code int    `json:"code"`
    Msg  string `json:"msg"`
    Data T      `json:"data,omitempty"`
}

// 响应内容
type ResponseBody struct {
    UserID   int
    Username string
    SysIP    string
    SysUser  string
}

// 请求体
type RequestBody struct {
    SysIP   string `json:"sys_ip" binding:"required,ipv4"`
    SysUser string `json:"sys_user" binding:"required,enum=root admin user"`
}

// 请求参数
type RequestParams struct {
    UserID int `form:"user_id" binding:"required,min=1,max=1000"` // 取值 [1, 1000]
    //UserID int `form:"user_id" binding:"required,gt=1,lt=1000"` // 取值 (1, 1000)
}

// 请求头
type RequestHeaders struct {
    Authorization string `header:"Authorization"`
}

// BearerToken 从请求头中获取 Bearer token
func (h RequestHeaders) BearerToken(c *gin.Context) (string, error) {
    var headers RequestHeaders
    if err = c.ShouldBindHeader(&headers); err != nil {
       return "", err
    }

    // 检查 Authorization 字段是否以 "Bearer " 开头
    if !strings.HasPrefix(headers.Authorization, "Bearer ") {
       return "", errors.New("No Bearer token found in Authorization header")
    }

    bearerToken := strings.TrimPrefix(headers.Authorization, "Bearer ")
    return bearerToken, nil
}

/*
    RouterFunc
*/

func HandleRequest(c *gin.Context) {
    // 解析请求头
    var bearerToken string
    bearerToken, err = RequestHeaders{}.BearerToken(c)
    if err != nil {
       c.JSON(http.StatusBadRequest, gin.H{"message": "Invalid request headers", "error": err.Error()})
       return
    }

    // parseJWT 操作
    slog.Info(bearerToken, "bearer token")

    // 解析请求参数
    var query RequestParams
    err = c.ShouldBindQuery(&query)
    if err != nil {
       c.JSON(http.StatusBadRequest, gin.H{"message": "Invalid request parameters", "error": err.Error()})
       return
    }
    userID := query.UserID

    // 解析请求体
    var body RequestBody
    err = c.ShouldBindJSON(&body)
    if err != nil {
       c.JSON(http.StatusBadRequest, gin.H{"message": "Invalid request body", "error": err.Error()})
       return
    }
    sysUser := body.SysUser
    sysIP := body.SysIP

    // 数据库操作
    var username string
    username, err = GetUserByIDFromDB(userID)
    if err != nil {
       c.JSON(http.StatusBadRequest, gin.H{"message": "No database entries", "error": err.Error()})
       return
    }

    // 返回响应
    c.JSON(http.StatusOK, Restful[ResponseBody]{
       Code: 0,
       Msg:  "success",
       Data: ResponseBody{
          UserID:   userID,
          Username: username,
          SysIP:    sysIP,
          SysUser:  sysUser,
       },
    })
}

/*
    Main
*/

func main() {
    r := Router()
    r.Run(":8080")
}

// ----------------------------------------------------------------

/*
    Validator
*/

// 自定义Enum类型校验器
func EnumValidator(fl validator.FieldLevel) bool {
    validValues := strings.Split(fl.Param(), " ")
    value := fl.Field().String()
    for _, validValue := range validValues {
       if value == validValue {
          return true
       }
    }
    return false
}

/*
    Router
*/

// Router 返回一个已配置好的 *gin.Engine 实例,用于处理 HTTP 请求路由
func Router() *gin.Engine {
    // 注册自定义验证器
    v, _ := binding.Validator.Engine().(*validator.Validate)
    v.RegisterValidation("enum", EnumValidator)

    // 设置 Gin 框架运行模式为发布模式
    gin.SetMode(gin.ReleaseMode)

    // 创建一个新的 Gin 实例
    r := gin.New()

    // 启用路径末尾斜杠的重定向(例如:/user/ 重定向到 /user)
    r.RedirectTrailingSlash = true

    // 处理没有匹配到路由的情况,返回 404 Not Found
    r.NoRoute(func(c *gin.Context) {
       c.String(http.StatusNotFound, "Not found")
       return
    })

    // 加载中间件
    r.Use(gin.Recovery(), PanicHandler())

    // 注册路由
    r.POST("/user", HandleRequest)

    return r
}

/*
    Middleware
*/

func PanicHandler() gin.HandlerFunc {
    return func(c *gin.Context) {
       defer func() {
          if err := recover(); err != nil {
             // 详细的错误分类,分而治之
             c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{
                "code":  50000,
                "msg":   "failed",
                "error": err,
             })
          }
       }()
       c.Next()
    }
}

/*
    ORM
*/

// 从数据库中查询用户信息
func GetUserByIDFromDB(userID int) (string, error) {
    users := map[int]string{
       1:    "John",
       2:    "Mary",
       3:    "David",
       40:   "Sarah",
       50:   "Michael",
       60:   "Emma",
       700:  "James",
       800:  "Olivia",
       900:  "William",
       1000: "Sophia",
    }

    if name, ok := users[userID]; ok {
       return name, nil
    }

    return "", errors.New("No matching user found")
}

中间件加载执行顺序

搞清楚 Gin 中间件的加载顺序非常重要。通过运行下面代码可知,如果我们想要使用 Gin 中间件来完成全局的错误处理,需要将错误处理中间件放在执行链的首位 defer 代码块中。

package main

import (
    "fmt"
    "log"

    "github.com/gin-gonic/gin"
)

func Md1() gin.HandlerFunc {
    return func(c *gin.Context) {
       fmt.Println("--->111")

       defer func() {
          fmt.Println("<---111")
       }()

       c.Next()
    }
}

func Md2() gin.HandlerFunc {
    return func(c *gin.Context) {
       fmt.Println("--->222")

       defer func() {
          fmt.Println("<---222")
       }()

       c.Next()
    }
}

func Md3() gin.HandlerFunc {
    return func(c *gin.Context) {
       fmt.Println("--->333")

       defer func() {
          fmt.Println("<---333")
       }()

       c.Next()
    }
}

func main() {
    router := gin.Default()

    router.Use(Md1(), Md2(), Md3())

    router.GET("/", func(c *gin.Context) {
       log.Println("hello world")
    })

    _ = router.Run(":8080")
}
[GIN-debug] Listening and serving HTTP on :8080
--->111
--->222
--->333
2023/09/01 11:47:36 hello world
<---333
<---222
<---111
[GIN] 2023/09/01 - 11:47:36 | 200 |    1.200041ms |       127.0.0.1 | GET      "/"

上下文键值对存储

c.Set()c.Request.WithContext(ctx) 的作用是相同的,只是它们涉及到不同的上下文。

  • c.Set() 方法用于在 GinContext 中存储键值对数据,这些数据只在当前请求的处理范围内有效,并且可以通过 c.Get()c.MustGet() 方法进行访问和检索。
func(c *gin.Context) {
    c.Set("user", &User{...}) // 将信息存储在 Context 中

    userinfo := c.MustGet("user").(*User) // 获取存储的信息
}
  • c.Request.WithContext(ctx) 方法用于替换当前请求的上下文,创建一个新的具有指定上下文的请求对象,并将该对象赋值给 c.Request。这样做的目的是在整个请求处理过程中传递上下文数据,使数据能够在请求链中的各个处理器和中间件之间共享。
func(c *gin.Context) {
    ctx := c.Request.Context()
    ctx = context.WithValue(ctx, "user", &User{...}) // 将信息存储在请求的上下文中
    c.Request = c.Request.WithContext(ctx)
}