路由

概述

go-web 框架在 Gin 之上提供了一层路由抽象,通过 RouterContextInterface 屏蔽底层框架细节。所有路由 handler 统一使用 HandlerFunc 签名,不直接依赖 *gin.Context

路由系统支持三种路由模式:

  • 标准路由 — 基于 Gin 的精确路径匹配(含路径参数 :id)
  • 路由分组 — 共享前缀和中间件的路由组
  • 正则路由 — 基于正则表达式的高级路由匹配,支持命名捕获组

快速开始

路由在 config/autoload/router.go 中通过 InitConfig 注册:

package autoload

import (
    httpInterfaces "cnb.cool/mliev/open/go-web/pkg/server/http_server/interfaces"
    "cnb.cool/mliev/open/go-web/app/controller"
)

type Router struct{}

func (receiver Router) InitConfig() map[string]any {
    return map[string]any{
        "http.router": func(router httpInterfaces.RouterInterface) {
            router.GET("/", controller.IndexController{}.GetIndex)
            router.GET("/health", controller.HealthController{}.GetHealth)
        },
    }
}

路由注册

RouterInterface 提供以下 HTTP 方法注册:

router.GET(path string, handler HandlerFunc)
router.POST(path string, handler HandlerFunc)
router.PUT(path string, handler HandlerFunc)
router.DELETE(path string, handler HandlerFunc)
router.PATCH(path string, handler HandlerFunc)
router.HEAD(path string, handler HandlerFunc)
router.OPTIONS(path string, handler HandlerFunc)

路径参数使用 Gin 的 :param 语法:

router.GET("/users/:id", controller.UserController{}.GetUser)

在 handler 中通过 c.Param("id") 获取参数值。

路由分组

使用 Group 方法创建共享前缀的路由组:

"http.router": func(router httpInterfaces.RouterInterface) {
    // /api/v1/users, /api/v1/posts ...
    v1 := router.Group("/api/v1")
    {
        v1.GET("/users", controller.UserController{}.List)
        v1.POST("/users", controller.UserController{}.Create)
    }

    // 嵌套分组
    health := router.Group("/health")
    {
        health.GET("", controller.HealthController{}.GetHealth)
        health.GET("/simple", controller.HealthController{}.GetHealthSimple)
    }
}

分组支持独立的中间件:

admin := router.Group("/admin")
admin.Use(middleware.AuthMiddleware())
{
    admin.GET("/dashboard", controller.AdminController{}.Dashboard)
}

正则路由

当 Gin 内置的路径参数无法满足需求时,使用 RegexGroup 创建正则路由组:

"http.router": func(router httpInterfaces.RouterInterface) {
    regex := router.RegexGroup("/api")
    {
        // 匹配 /api/users/123
        regex.GET(`^/api/users/(?P<id>\d+)$`, controller.UserController{}.GetUser)

        // 匹配 /api/posts/456/comments
        regex.POST(`^/api/posts/(?P<postId>\d+)/comments$`, controller.PostController{}.AddComment)

        // 匹配所有 HTTP 方法
        regex.Any(`^/api/proxy/(?P<path>.+)$`, controller.ProxyController{}.Handle)
    }
}

正则路由特性

  • 命名捕获组:(?P<name>pattern) 中的 name 会自动写入路径参数,通过 c.Param("name") 获取

  • Handler 链:正则路由支持多个 handler,依次执行:

    regex.GET(`^/api/resource/(?P<id>\d+)$`, authHandler, resourceHandler)
  • 惰性挂载:同一前缀下的所有正则路由共享一个 Gin 通配路由,首次注册时自动挂载

  • 无匹配返回 404:请求未命中任何正则规则时返回 404 not found

可用方法

regex.GET(pattern string, handlers ...HandlerFunc)
regex.POST(pattern string, handlers ...HandlerFunc)
regex.PUT(pattern string, handlers ...HandlerFunc)
regex.DELETE(pattern string, handlers ...HandlerFunc)
regex.PATCH(pattern string, handlers ...HandlerFunc)
regex.HEAD(pattern string, handlers ...HandlerFunc)
regex.OPTIONS(pattern string, handlers ...HandlerFunc)
regex.Any(pattern string, handlers ...HandlerFunc)    // 匹配所有 HTTP 方法

中间件

全局中间件

config/autoload/middleware.go 中注册全局中间件:

package autoload

import (
    httpInterfaces "cnb.cool/mliev/open/go-web/pkg/server/http_server/interfaces"
    "cnb.cool/mliev/open/go-web/app/middleware"
)

type Middleware struct{}

func (receiver Middleware) InitConfig() map[string]any {
    return map[string]any{
        "http.middleware": []httpInterfaces.MiddlewareFunc{
            middleware.CorsMiddleware(),
        },
    }
}

分组中间件

通过 Use 方法为路由组添加中间件:

api := router.Group("/api")
api.Use(middleware.AuthMiddleware())

编写自定义中间件

中间件签名为 MiddlewareFunc,即 func(RouterContextInterface)。必须调用 c.Next() 继续执行后续 handler,或调用 c.Abort() / c.AbortWithStatus() 中止请求。

package middleware

import (
    "net/http"
    httpInterfaces "cnb.cool/mliev/open/go-web/pkg/server/http_server/interfaces"
)

func AuthMiddleware() httpInterfaces.MiddlewareFunc {
    return func(c httpInterfaces.RouterContextInterface) {
        token := c.GetHeader("Authorization")
        if token == "" {
            c.AbortWithStatus(http.StatusUnauthorized)
            return
        }

        // 验证 token,将用户信息存入上下文
        c.Set("userId", "12345")
        c.Next()
    }
}

Panic Recovery

框架默认挂载了一个自定义的 panic recovery 中间件,任何 handler 中的 panic 都会:

  1. 记录完整堆栈到日志(通过请求级 logger,带 traceId)
  2. 向客户端返回统一格式的 500 响应,而不是默认的空响应或裸异常

业务代码无需关心,只在确实需要捕获时再写 defer recover()

控制器编写

Handler 签名

所有路由 handler 的签名为:

type HandlerFunc func(RouterContextInterface)

控制器结构

推荐嵌入 BaseResponse 以使用统一的响应方法:

package controller

import (
    "net/http"
    httpInterfaces "cnb.cool/mliev/open/go-web/pkg/server/http_server/interfaces"
)

type UserController struct {
    BaseResponse
}

func (receiver UserController) GetUser(c httpInterfaces.RouterContextInterface) {
    id := c.Param("id")
    // 业务逻辑...
    receiver.Success(c, map[string]any{"id": id, "name": "张三"})
}

BaseResponse 响应方法

// 成功响应 → {"code": 0, "message": "ok", "data": ...}
receiver.Success(c, data)

// 成功响应(自定义消息)
receiver.SuccessWithMessage(c, "操作成功", data)

// 错误响应 → {"code": 1001, "message": "参数错误"}
receiver.Error(c, constants.ErrCodeInvalidParams, "参数错误")

// 错误响应(携带数据)
receiver.ErrorWithData(c, 400, "验证失败", validationErrors)

错误码为 400-599 范围时,HTTP 状态码与错误码一致;否则 HTTP 状态码为 200。

请求上下文 API 速查表

RouterContextInterface(定义于 pkg/server/http_server/interfaces/router.go)提供以下方法:

响应

方法 说明
JSON(code int, obj any) 返回 JSON 响应
HTML(code int, name string, obj any) 渲染 HTML 模板
String(code int, format string, values ...any) 返回文本响应
Data(code int, contentType string, data []byte) 返回原始字节
Redirect(code int, location string) HTTP 重定向
File(filepath string) 直接以文件返回
Stream(step func(io.Writer) bool) 流式响应,适合 SSE / 大文件
Status(code int) 仅设置状态码,不写 body

请求参数

方法 说明
Param(key string) string 获取路径参数(:id 或正则命名捕获组)
Query(key string) string 获取 URL 查询参数
DefaultQuery(key, defaultValue string) string 查询参数不存在时返回默认值
QueryArray(key string) []string 同名查询参数全部值
PostForm(key string) string 获取表单数据
DefaultPostForm(key, defaultValue string) string 表单值不存在时返回默认值
ShouldBind(obj any) error 自动选择 binding(根据 Content-Type)
ShouldBindJSON(obj any) error 解析 JSON 请求体
ShouldBindQuery(obj any) error 把 query string 绑定到结构体
GetRawData() ([]byte, error) 读取原始请求体

文件上传

方法 说明
FormFile(name string) (*multipart.FileHeader, error) 获取上传文件头
SaveUploadedFile(file *multipart.FileHeader, dst string) error 保存到磁盘

HTTP 头与 Cookie

方法 说明
GetHeader(key string) string 获取请求头
SetHeader(key, value string) 设置响应头
Cookie(name string) (string, error) 读取 Cookie
SetCookie(name, value string, maxAge int, path, domain string, secure, httpOnly bool) 设置 Cookie
SetSameSite(mode http.SameSite) 设置后续 SetCookie 的 SameSite

请求信息

方法 说明
Path() string 请求路径
FullPath() string 完整路由模式
Method() string HTTP 方法
ClientIP() string 客户端 IP(经过 X-Forwarded-For 解析)
RemoteAddr() string 原始 TCP 远端地址
Host() string 请求 Host(含端口)
Hostname() string Host 中的主机名部分
Scheme() string http / https
URL() string 完整 URL
ContentType() string 请求 Content-Type
UserAgent() string User-Agent 头
Referer() string Referer 头
IsWebsocket() bool 是否为 WebSocket 升级请求

上下文存取

方法 说明
Set(key string, value any) 存储值到上下文
Get(key string) any 取值
GetString(key string) string 取字符串值

错误与流程控制

方法 说明
Error(err error) 把错误附加到上下文,框架统一记录日志
IsAborted() bool 是否已被 Abort
Next() 执行下一个 handler(中间件中使用)
Abort() 中止后续 handler
AbortWithStatus(code int) 中止并返回指定 HTTP 状态码

响应状态

方法 说明
Written() bool 响应是否已写出
GetStatus() int 当前响应状态码

请求级 Logger

框架在请求开始时会向上下文注入一个携带 traceId 的 Logger,业务代码通过:

log := helper.GetRequestLogger(c)
log.Info("处理请求", gsr.String("user_id", userID))

获取该 Logger。这样所有该请求范围内的日志都会带上同一个 traceId,方便排查。详见「Helper 全局访问器」章节。