下载工作台
Gin Web 开发

路由、参数与中间件

试读上半部分 · 解锁后可读全文

第 2 章 · 路由、参数与中间件

本章目标:搭建 api-go-demo/api/v1 路由组;绑定路径参数、查询参数、JSON Body;实现 RequestIDRecoveryCORS 中间件;将 ch01 单文件重构为多包结构;提供本章结束时可运行的完整代码;对照 fastapi-web ch02 APIRouter 与 flask-web ch02 Blueprint。

学时建议:3~4 小时(含 2 小时跟练)

前置:完成 gin-web ch01go-dev ch12(HTTP)。


2.1 场景说明:商品 REST 路由骨架

api-go-demo 需对外提供与 svc-demoapi-demo 风格一致的商品读接口,字段统一为 slug + is_published + price(分)

方法路径认证说明
GET/health健康检查
GET/api/v1/products列表(默认仅 is_published=true
GET/api/v1/products/:slug按 slug 详情
POST/api/v1/products无(ch06 改 admin)创建商品(本章 mock 返回)
ch01 main.go 单文件
        │
        ▼ 本章重构
api-go-demo/
├── cmd/server/main.go
└── internal/
    ├── router/router.go
    ├── handler/product.go
    └── middleware/
        ├── request_id.go
        ├── recovery.go
        └── cors.go

对照章节

主题flask-web ch02fastapi-web ch02gin-web ch02
路由模块化BlueprintAPIRouterGroup + handler 包
公共逻辑before_requestDepends中间件链
版本前缀url_prefixprefix=r.Group("/api/v1")

2.2 逐步操作表

步骤操作验证
1创建 internal/router/router.go包可编译
2创建 internal/handler/product.goHandler 方法签名正确
3创建三个 middleware 文件各自 package middleware
4修改 cmd/server/main.go 调用 router.Newgo build 无错
5go run ./cmd/server监听 8080
6curl -i GET /api/v1/products/go-handbook200 + X-Request-ID
7POST JSON 创建商品201 或 400 校验
8OPTIONS 预检204

2.3 路由组与 Engine 初始化

// internal/router/router.go
package router

import (
    "net/http"

    "github.com/gin-gonic/gin"
    "example.com/api-go-demo/internal/handler"
    "example.com/api-go-demo/internal/middleware"
)

func New(h *handler.ProductHandler) *gin.Engine {
    r := gin.New()
    r.Use(gin.Logger())
    r.Use(middleware.Recovery())
    r.Use(middleware.RequestID())
    r.Use(middleware.CORS())

    r.GET("/health", func(c *gin.Context) {
        c.JSON(http.StatusOK, gin.H{"status": "ok"})
    })

    v1 := r.Group("/api/v1")
    {
        v1.GET("/products", h.ListProducts)
        v1.GET("/products/:slug", h.GetProduct)
        v1.POST("/products", h.CreateProduct)
    }
    return r
}
设计点说明
gin.New()不用 Default,避免重复 Logger
中间件顺序Recovery → RequestID → CORS → 路由
Group("/api/v1")版本化 API,与 Flask/FastAPI 前缀一致

fastapi-web ch02 等价写法:

router = APIRouter(prefix="/api/v1/products", tags=["商品"])
app.include_router(router)

2.4 ProductHandler 完整代码

// internal/handler/product.go
package handler

import (
    "net/http"
    "strconv"

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

type ProductHandler struct {
    // ch05 注入 *service.ProductService
}

func NewProductHandler() *ProductHandler {
    return &ProductHandler{}
}

// GET /api/v1/products
func (h *ProductHandler) ListProducts(c *gin.Context) {
    onlyPub := c.DefaultQuery("published", "true") == "true"
    page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
    pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "20"))
    if page < 1 {
        page = 1
    }
    if pageSize < 1 || pageSize > 100 {
        pageSize = 20
    }

    // ch04/ch05 接数据库;本章 mock
    items := []gin.H{}
    if onlyPub {
        items = append(items, gin.H{
            "slug": "go-handbook", "name": "Go 手册",
            "price": 6800, "is_published": true, "stock": 50,
        })
    }
    rid, _ := c.Get("request_id")
    c.JSON(http.StatusOK, gin.H{
        "items":       items,
        "total":       len(items),
        "page":        page,
        "page_size":   pageSize,
        "request_id":  rid,
        "only_public": onlyPub,
    })
}

// GET /api/v1/products/:slug
func (h *ProductHandler) GetProduct(c *gin.Context) {
    slug := c.Param("slug")
    if slug == "" {
        c.JSON(http.StatusBadRequest, gin.H{
            "code": "VALIDATION_ERROR", "message": "slug required",
        })
        return
    }
    // mock:仅 go-handbook 存在
    if slug != "go-handbook" {
        c.JSON(http.StatusNotFound, gin.H{
            "code": "NOT_FOUND", "message": "product not found",
        })
        return
    }
    c.JSON(http.StatusOK, gin.H{
        "slug":          slug,
        "name":          "Go 手册",
        "price":         6800,
        "is_published":  true,
        "stock":         50,
    })
}

// POST /api/v1/products
type CreateProductReq struct {
    Slug        string `json:"slug" binding:"required,min=2,max=64"`
    Name        string `json:"name" binding:"required,max=200"`
    Price       int64  `json:"price" binding:"required,gte=0"`
    Stock       int    `json:"stock" binding:"gte=0"`
    IsPublished bool   `json:"is_published"`
}

func (h *ProductHandler) CreateProduct(c *gin.Context) {
    var req CreateProductReq
    if err := c.ShouldBindJSON(&req); err != nil {
        c.JSON(http.StatusBadRequest, gin.H{
            "code": "VALIDATION_ERROR", "message": err.Error(),
        })
        return
    }
    c.JSON(http.StatusCreated, req)
}

2.5 参数绑定对照表

类型Gin API示例 URL / BodyFastAPI 等价Flask 等价
路径c.Param("slug")/products/go-handbook路径参数<slug>
查询c.Query / DefaultQuery?published=true&page=1Query(...)request.args
JSONc.ShouldBindJSONPOST bodyPydantic 模型get_json()
Headerc.GetHeaderAuthorizationHeader(...)request.headers
Formc.ShouldBindmultipart/formForm(...)request.form

ShouldBind 系列

方法行为
ShouldBindJSON失败返回 error,不 abort
BindJSON失败自动 400 并 abort
ShouldBindQuery绑定查询到 struct

本章选用 ShouldBindJSON 以便自定义错误 JSON 格式(ch07 统一 envelope)。


2.6 RequestID 中间件

// internal/middleware/request_id.go
package middleware

以下内容需解锁后阅读

试读已结束。解锁本章 ¥5.00,或开通年度会员畅读全部教程。
年度会员 ¥199.00/年; 小紫 AI 工作台有效会员 ¥99.00/年

正文仅在服务端鉴权后下发,未付费无法获取下半部分内容。