Skip to content

第三章 Web框架(Gin) v1.0

第一部分 快速搭建

一、项目初始化

go
# 初始化Go模块
go mod init app
# 安装Gin框架
go get -u github.com/gin-gonic/gin

二、基本结构

下面示例创建了一个简单的 Gin 应用程序

go
package main

import (
	"github.com/gin-gonic/gin"
	"net/http"
)

func main() {
    // 创建基础引擎
	r := gin.New()
	
	helloGroup := r.Group("/hello")
	{
		helloGroup.GET("/", func(c *Context) {
			c.JSON(http.StatusOK, gin.H{
				"code": 0,
				"msg":  "hello",
			})
		})
	}

	// 启动服务器,默认监听8080端口
	r.Run(":8080")
}

第二部分 引擎与路由

一、Gin引擎

Engine 是 Gin 应用的实例,相当于整个 Web 应用的 "大脑",负责管理路由、中间件、配置选项等核心功能

go
// 创建一个默认配置的、全新的、无默认中间件的引擎实例
engine := gin.New()

// 创建默认配置的、全新的、带默认日志(Logger)和错误恢复(Recovery)中间件的引擎实例
// 本质上是先调用New()创建基础引擎,然后通过Use()方法添加中间件
engine := gin.Default()
go
// 从 New() 方法源码可以具体查看其默认配置,也可以根据项目情况进行高度定制
func New(opts ...OptionFunc) *Engine {
	debugPrintWARNINGNew()
	engine := &Engine{
	    // 创建了一个空的路由组作为根路由
		RouterGroup: RouterGroup{
			Handlers: nil,
			basePath: "/",
			root:     true,
		},
		FuncMap:                template.FuncMap{},  // 初始化模板函数映射(用于HTML渲染)
		RedirectTrailingSlash:  true,   // 自动重定向带斜杠的路由到不带斜杠的版本(如/foo/重定向到/foo)
		RedirectFixedPath:      false,  // 禁用错误路径自动修复
		HandleMethodNotAllowed: false,  // 不处理HTTP方法不允许的情况(默认返回404)
		ForwardedByClientIP:    true,   // 允许通过请求头获取客户端真实IP
		RemoteIPHeaders:        []string{"X-Forwarded-For", "X-Real-IP"},  // 用于获取客户端IP的请求头列表
		TrustedPlatform:        defaultPlatform,  // 设置默认的可信平台(用于获取客户端IP)
		UseRawPath:             false,  // 不使用原始 URL 路径,会对 URL 进行转义处理
		RemoveExtraSlash:       false,  // 不移除URL中的额外斜杠(如//foo///bar不会转为/foo/bar)
		UnescapePathValues:     true,   // 自动对路径参数进行 URL 解码(如将%20转为空格)
		MaxMultipartMemory:     defaultMultipartMemory,  // 设置处理文件上传的默认内存限制(32MB)
		trees:                  make(methodTrees, 0, 9),  // 初始化路由树(支持9种HTTP方法)
		delims:                 render.Delims{Left: "{{", Right: "}}"},  // 设置默认的 HTML 模板分隔符( "{{" "}}" )
		secureJSONPrefix:       "while(1);",  // 为JSON响应添加安全前缀(防止JSON劫持)
		trustedProxies:         []string{"0.0.0.0/0", "::/0"},  // 默认信任所有代理IP(适用于开发环境)
		trustedCIDRs:           defaultTrustedCIDRs,   // 默认的可信CIDR范围(用于IP验证)
	}
	engine.RouterGroup.engine = engine
	// 初始化上下文对象池(sync.Pool),初始容量由maxParams决定
	engine.pool.New = func() any {
		return engine.allocateContext(engine.maxParams)
	}
	return engine.With(opts...)
}

二、路由注册

使用 Gin 定义路由方式简洁,通过 HTTP 方法来注册处理函数

go
// 所有HTTP方法路由(如 GET/POST)的底层实现
func (group *RouterGroup) Handle(httpMethod string, relativePath string, handlers ...HandlerFunc) IRoutes
// 一次性为指定路径注册所有标准HTTP方法的路由
func (group *RouterGroup) Any(relativePath string, handlers ...HandlerFunc) IRoutes
// 为指定路径注册指定HTTP方法的路由
func (group *RouterGroup) GET(relativePath string, handlers ...HandlerFunc) IRoutes
func (group *RouterGroup) POST(relativePath string, handlers ...HandlerFunc) IRoutes
func (group *RouterGroup) DELETE(relativePath string, handlers ...HandlerFunc) IRoutes
func (group *RouterGroup) PATCH(relativePath string, handlers ...HandlerFunc) IRoutes
func (group *RouterGroup) PUT(relativePath string, handlers ...HandlerFunc) IRoutes
func (group *RouterGroup) OPTIONS(relativePath string, handlers ...HandlerFunc) IRoutes
func (group *RouterGroup) HEAD(relativePath string, handlers ...HandlerFunc) IRoutes

// 将单个本地文件映射到指定的路由路径,客户端访问该路径时直接返回对应的文件
func (group *RouterGroup) StaticFile(relativePath string, filepath string) IRoutes
// 通过自定义 http.FileSystem 读取文件,其他作用同 StaticFile 方法
func (group *RouterGroup) StaticFileFS(relativePath string, filepath string, fs http.FileSystem) IRoutes
// 将整个本地目录映射到指定路由路径,客户端可通过该路径访问目录下的所有文件(支持子目录递归访问)
func (group *RouterGroup) Static(relativePath string, root string) IRoutes
// 通过自定义 http.FileSystem 读取文件,其他作用同 Static 方法
func (group *RouterGroup) StaticFS(relativePath string, fs http.FileSystem) IRoutes
go
package main

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

func main() {
    r := gin.Default()
    r.GET("/user/:id", func(c *Context) {
        // 获取路径参数, :id 为路径参数占位符 
        id := c.Param("id")
        c.JSON(200, gin.H{"user_id": id})
    })
    r.Run()
}

二、分组路由

Gin 支持路由分组,相关路由可拥有共同前缀并共享中间件

go
// 创建一个新的路由组,为该路由组添加共同 relativePath 前缀,并共享中间件
Group(relativePath string, handlers ...HandlerFunc) *RouterGroup
go
package main

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

func main() {
    // 根路由组
    r := gin.Default()

    // 创建用户模块路由组,路径前缀:/user
    userGroup := r.Group("/user")
    // 组内路由,实际路径:/user/login
    userGroup.POST("/login", func(c *Context) {
        c.JSON(200, gin.H{"msg": "user login"})
    })
    // 组内路由,实际路径:/user/profile
    userGroup.GET("/profile", func(c *Context) {
        c.JSON(200, gin.H{"msg": "user profile"})
    })

    r.Run()
}

第二部分 上下文与中间件

Gin 中的上下文由 *gin.Context 类型表示,是请求处理的核心载体,贯穿整个请求生命周期,负责在请求、中间件和处理函数之间传递数据、管理流程

一、核心作用

  1. 数据传递:
    • 通过 c.Set(key, value) 在中间件或处理函数中存储数据,通过 c.Get(key) 或 c.MustGet(key) 在后续流程中获取,实现不同处理阶段的数据共享(如用户身份信息、请求 ID 等)。
  2. 流程控制:
    • c.Next():暂停当前中间件,执行后续中间件或处理函数,完成后返回继续执行当前中间件剩余逻辑(用于 “前置处理 - 后置处理” 场景)。
    • c.Abort():中断请求流程,不再执行后续中间件或处理函数(常用于权限校验失败等场景)。
    • c.AbortWithStatus(code):中断流程并返回指定 HTTP 状态码。
  3. 请求与响应管理
    • 封装了 HTTP 请求(c.Request)和响应(c.Writer),提供便捷方法操作请求参数(如 c.Param()、c.Query())、请求头(c.GetHeader())、Cookie(c.Cookie())等
    • 提供响应方法(如 c.JSON()、c.String()、c.HTML()),快速返回不同格式的响应

二、中间件

中间件实际是 gin.HandlerFunc 类型的函数,用于在请求到达处理函数之前或之后执行通用逻辑

1、定义与使用

go
// 直接定义的中间件函数
func MiddlewareName(c *Context) {
   // 前置处理逻辑(请求到达处理函数前执行)
   
   c.Next() // 调用后续的中间件或处理函数
   
   // 后置处理逻辑(处理函数执行完成后执行) 
}
// 工厂函数(推荐使用,支持更复杂场景)
func MiddlewareFuncName() gin.HandlerFunc {
    return func(c *Context) {
        // 前置处理逻辑(请求到达处理函数前执行)
        
        c.Next() // 调用后续的中间件或处理函数
        
        // 后置处理逻辑(处理函数执行完成后执行)
    }
}
go
r := gin.New()
// 全局注册LoggerMiddleware中间件
r.Use(LoggerMiddleware())
go
// 创建路由组并应用中间件
apiGroup := r.Group("/api", FirstMiddleware(), SecondMiddleware())
// 通过Use方法为已创建的路由组添加中间件
apiGroup.Use(OtherMiddleware())
go
// 为单路由注册AuthMiddleware中间件
r.GET("/profile", AuthMiddleware(), func(c *Context) {
  c.JSON(200, gin.H{"msg": "message"})
})

2、流程控制

go
/// 后续处理 
// 执行后续的中间件和处理器,之后返回继续执行当前中间件
func (c *Context) Next()

/// 中断处理
// 中断请求处理,不再执行后续中间件和处理器
func (c *Context) Abort()
// 中断并返回指定状态码
func (c *Context) AbortWithStatus(code int)
// 中断并返回指定状态码和JSON数据
func (c *Context) AbortWithStatusJSON(code int, jsonObj any)
// 中断请求流程并记录错误
func (c *Context) AbortWithError(code int, err error) *Error

三、数据传递

go
// 向当前上下文中存储键值对数据,数据仅在当前请求的生命周期内有效
func (c *Context) Set(key string, obj any)
// 从上下文中获取指定键对应的值,并返回一个布尔值表示该键是否存在
func (c *Context) Get(key string) (any, bool)
// 从上下文中获取指定键对应的值,若键不存在则直接 panic
func (c *Context) MustGet(key string) any
go
// 存储数据到上下文
c.Set("user_id", 123)
c.Set("username", "admin")

// 获取数据
if userId, exists := c.Get("user_id"); exists {
    // 类型断言
    id := userId.(int)
}

// 确保能获取到数据并进行类型断言
username := c.MustGet("username").(string)

第三部分 请求与响应

一、请求参数获取

直接从 URL/查询参数等 获取单个参数,返回字符串类型,适合简单参数场景

1、查询参数

go
// 获取参数,不存在则返回空字符串
func (c *Context) Query(key string) string
// 获取参数,不存在则返回默认值
func (c *Context) DefaultQuery(key string, defaultValue string) string
// 获取参数,同时返回布尔值表示是否存在
func (c *Context) GetQuery(key string) (string, bool)

2、路径参数

go
// 获取动态路径参数,不存在则返回空字符串
func (c *Context) Param(key string) string

3、获取请求头

go
// 获取指定的请求头字段值(键名不区分大小写),不存在则返回空字符串
func (c *Context) GetHeader(key string) string
go
// 获取Cookie信息,如果Cookie不存在或读取失败则返回错误
func (c *Context) Cookie(name string) (string, error)

二、请求参数绑定

将请求数据(JSON、表单、路径参数等)自动映射到 Go 结构体,支持数据验证,适合处理复杂参数

1、映射结构体

①结构体定义
go
type ExampleEnt struct {
    Feild  string `参数标签:"feild" binding:"结构体标签"`
}
go
type UserCreateRequestEnt struct {
    Id     string `uri:"id" binding:"required,min=2"`     // 绑定路径参数,必填,最少 2 个字符
    Name   string `json:"name" binding:"required,min=2"`  // 绑定json参数,必填,最少 2 个字符
    Email  string `json:"email" binding:"required,email"` // 绑定json参数,必填,邮箱格式
    Age    int    `json:"age" binding:"min=0,max=150"`    // 绑定json参数,数值范围
}
go
`json:"feild"`    // 对应 json 绑定,字段名与JSON中的键对应
`form:"feild"`    // 对应 查询参数 绑定,字段名与查询参数的键对应
`header:"feild"`  // 对应 请求头 绑定,字段名与请求头的键对应
`uri:"feild"`     // 对应 路径参数 绑定,字段名与路径参数的占位符对应
`xml:"feild"`     // 对应 xml格式数据 绑定
`yaml:"feild"`    // 对应 yaml格式数据 绑定
`toml:"feild"`    // 对应 toml格式数据 绑定
②参数校验

结构体标签(基于 validator 库)实现参数校验,常用标签:

  • required:必填项
  • omitempty:字段为空忽略验证
  • min/max:数值或字符串长度范围
  • email:邮箱格式
  • ip/ipv4/ipv6:必须是合法IP地址(v4或v6/v4/v6)
  • regexp=^pattern$:正则匹配

2、Bind与ShouldBind

go
/// Bind 系列方法 绑定参数到结构体,若解析失败,返回 400 Bad Request 并终止请求,简化错误处理逻辑
// 根据请求的 方法 和 Content-Type 自动选择绑定器,例如:POST/PUT 请求且 Content-Type="application/json" → 使用 JSON 解析;Content-Type="application/xml" → 使用 XML 解析
func (c *Context) Bind(obj any) error
// 强制使用JSON绑定,不检查Content-Type
func (c *Context) BindJSON(obj any) error
// 绑定查询参数到结构体
func (c *Context) BindQuery(obj any) error
// 绑定请求头参数到结构体
func (c *Context) BindHeader(obj any) error
// 绑定路径参数到结构体
func (c *Context) BindUri(obj any) error
// 强制使用XML绑定,不检查Content-Type
func (c *Context) BindXML(obj any) error
// 以YAML格式解析请求体,将数据绑定到结构体
func (c *Context) BindYAML(obj any) error
// 以TOML格式解析请求体,将数据绑定到结构体
func (c *Context) BindTOML(obj any) error
// 所有Bind方法的底层实现,使用指定的绑定器(如 binding.JSON)进行数据绑定,若解析失败,返回400错误并终止请求
func (c *Context) MustBindWith(obj any, b binding.Binding) error
go
// ShouldBind 系列方法 绑定参数到结构体,若解析失败,仅返回错误,不会自动向客户端响应
// 根据请求的 方法 和 Content-Type 自动选择绑定器,例如:POST/PUT 请求且 Content-Type="application/json" → 使用 JSON 解析;Content-Type="application/xml" → 使用 XML 解析
func (c *Context) ShouldBind(obj any) error
// 强制使用JSON绑定,不检查Content-Type
func (c *Context) ShouldBindJSON(obj any) error
// 绑定查询参数到结构体
func (c *Context) ShouldBindQuery(obj any) error
// 绑定请求头参数到结构体
func (c *Context) ShouldBindHeader(obj any) error
// 绑定路径参数到结构体
func (c *Context) ShouldBindUri(obj any) error
// 强制使用XML绑定,不检查Content-Type
func (c *Context) ShouldBindXML(obj any) error
// 以YAML格式解析请求体,将数据绑定到结构体
func (c *Context) ShouldBindYAML(obj any) error
// 以TOML格式解析请求体,将数据绑定到结构体
func (c *Context) ShouldBindTOML(obj any) error
// 所有ShouldBind方法的底层实现,使用指定的绑定器(如 binding.JSON)进行数据绑定,若解析失败,仅返回错误
func (c *Context) ShouldBindWith(obj any, b binding.Binding) error

三、请求响应

1、基础响应

go
// 设置响应状态码并返回格式化字符串响应
func (c *Context) String(code int, format string, values ...interface{})
// 设置响应状态码并返回 JSON 响应(自动设置 Content-Type: application/json)
func (c *Context) JSON(code int, obj interface{})
// 设置响应状态码并返回 XML 响应(自动设置 Content-Type: application/xml)
func (c *Context) XML(code int, obj interface{})
// 设置响应状态码并渲染 HTML 模板并返回(需预先加载模板)
func (c *Context) HTML(code int, name string, obj interface{})
// 所有响应方法的底层实现
func (c *Context) Render(code int, r render.Render) 
// 单独设置响应状态码,Render方法底层调用了该方法设置响应状态码
func (c *Context) Status(code int)

对于HTML响应,渲染HTML模板需要预先在Gin引擎中加载模板

go
// 加载符合pattern的多个模板,支持通配符*,如"templates/**/*"
func (engine *Engine) LoadHTMLGlob(pattern string)
// 精确指定需要加载的模板文件路径
func (engine *Engine) LoadHTMLFiles(files ...string)
go
/// 模板语法
// Gin框架默认使用的是Go语言标准库的模板语法(html/template包),在此不过多赘述
/// 常用模板语法 - 变量渲染
// {{.变量名(.变量属性)}

Gin模板的唯一标识是 define 定义的名称,若为定义则默认使用文件名;同时,加载同一唯一标识的模板会被后加载的模板覆盖

html
{{define "template1"}}
<h1>{{.template_name}}</h1>
{{end}}
go
r.LoadHTMLFiles("templates/index.html")

r.GET("/template", func(c *gin.Context) {
  c.HTML(http.StatusOK, "template1", gin.H{
       "template_name":  "template1",
   })
})

2、设置响应头

go
// 设置响应头字段
func (c *Context) Header(key, value string)
go
// 向客户端设置 Cookie
func (c *Context) SetCookie(
   name string,   // Cookie键名
   value string,  // Cookie值
   maxAge int,    // 有效期: 正数表示在maxAge秒后过期,客户端会自动删除; 负数(通常为-1)表示有效期为当前会话,关闭浏览器后自动失效; 0表示立即删除该Cookie
   path string,   // Cookie的生效路径,"/" 表示全站所有路径都生效,"/path" 表示仅在访问 /path 开头路径时携带,""表示当前请求的路径
   domain string, // 生效域名,限制Cookie仅在指定域名下被携带,""表示仅在当前域名生效,"domain.com"表示在domain.com及其子域名生效
   secure bool,   // 是否仅通过HTTPS协议传输,防止HTTP明文传输被窃听
   httpOnly bool  // 是否禁止JavaScript访问此Cookie,有效防止XSS(跨站脚本攻击)窃取Cookie
)

3、重定向

go
// 重定向到指定 URL(状态码通常为 301(StatusMovedPermanently) 或 302 (Found))
func (c *Context) Redirect(code int, location string)

4、文件响应

go
// 发送指定路径的文件(自动根据文件扩展名推断 MIME 类型)
func (c *Context) File(path string)
// 发送文件并指定下载时的文件名(触发浏览器下载)
func (c *Context) FileAttachment(path, filename string)
// 通过io.Reader流返回文件数据
func (c *Context) DataFromReader(
   code int,                         // HTTP 响应状态码
   contentLength int64,              // 文件大小 (字节数,-1表示未知)
   contentType string,               // MIME 类型
   reader io.Reader,                 // 文件数据的读取流 (如: os.File/bytes.Reader)
   extraHeaders map[string]string    // 额外的响应头 (如: Content-Disposition)
)

第四部分 进阶应用

一、运行与优雅退出

1、启动运行

在 Gin 框架中,r.Run() 是启动 HTTP 服务的常用方法,其本质是一个阻塞式调用(内部会启动 http.Server 并阻塞当前 goroutine)

优点:使用简单,r.Run() 封装了创建 http.Server、绑定地址、启动监听 等底层逻辑,适合快速开发
缺点:灵活性差,难以定制服务参数;无法实现优雅退出

go
if err := r.Run(); err != nil {
  log.Fatalf("run fail: %v", err)
}
go
func (engine *Engine) Run(addr ...string) (err error) {
    // 延迟执行错误打印函数:无论 Run 方法因何种原因退出,都会打印错误信息
	defer func() { debugPrintError(err) }()

	if engine.isUnsafeTrustedProxies() {
		debugPrint("[WARNING] You trusted all proxies, this is NOT safe. We recommend you to set a value.\n" +
			"Please check https://pkg.go.dev/github.com/gin-gonic/gin#readme-don-t-trust-all-proxies for details.")
	}
	
    // 解析监听地址:若未传入 addr,默认使用 ":8080"; 若传入多个 addr,取第一个有效地址
	address := resolveAddress(addr)
	debugPrint("Listening and serving HTTP on %s\n", address)
	
	// 调用标准库 http.ListenAndServe 启动服务
	err = http.ListenAndServe(address, engine.Handler())
	return
}

2、实现优雅退出

通过 http.Server 实例的 ListenAndServe() 启动服务,从而可以调用 Shutdown() 方法实现优雅关闭。

go
func main() {
	// 创建可被系统信号取消的根上下文
	globalCtx, globalCancel := signal.NotifyContext(
		context.Background(),
		syscall.SIGINT,  // Ctrl+C
		syscall.SIGTERM, // kill 命令
		syscall.SIGQUIT, // Ctrl+\
	)
	defer globalCancel() // 程序退出前触发全局清理

	// 初始化Gin引擎及路由
	r := gin.Default()

	// 显式创建http.Server
	server := &http.Server{
		Addr:    ":8080", // 监听地址
		Handler: r,       // 绑定Gin引擎
	}

	// 启动服务器,默认监听8080端口
	// 启动Gin服务(非阻塞)
	go func() {
		// 启动服务
		if err := server.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
			os.Exit(1) // 启动失败时退出程序
		}
	}()

	// 等待全局退出信号(如Ctrl+C、kill)
	<-globalCtx.Done()

	// 关闭Gin服务(等待正在处理的请求完成,超时5秒)
	shutdownCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
	defer cancel()

	// 调用Shutdown,通知服务停止接受新请求并等待现有请求完成
	if err := server.Shutdown(shutdownCtx); err != nil {
		os.Exit(1)
	}
}

二、中间件应用示例

编写一个密码认证中间件,模拟实现一个登录后需要二次认证的功能

应用场景:重要资源需要二次输入密码验证后方可访问

http
# 定义环境
@dev_baseUrl = http://localhost:8080
# 默认使用开发环境
@baseUrl = {{dev_baseUrl}}

### 登录接口
POST {{baseUrl}}/user/login
Content-Type: application/json

{
  "username": "admin",
  "password": "admin"
}

### 校验密码接口
POST {{baseUrl}}/user/verify
Content-Type: application/json

{
  "username": "admin",
  "password": "admin"
}

### 访问限制资源接口
GET {{baseUrl}}/user/hello
Accept: application/json

# 直接调用/登录后直接调用 -> (500) {"msg": "get verify app fail"} [访问被拒绝]
# 登录后验证密码后调用 -> (200) {"msg": "hello"} [正常访问资源]
go
package main

import (
	"context"
	"errors"
	"net/http"
	"os"
	"time"

	"example/app"
	"example/middleware/verify"

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

type LoginEeqEnt struct {
	Username string `json:"username" `
	Password string `json:"password"`
}

type VerifyPasswordReqEnt struct {
	Password string `json:"password"`
}

func Login(c *gin.Context) {
	req := LoginEeqEnt{}
	// 获取请求参数
	if err := c.ShouldBindJSON(&req); err != nil {
		verify.GetMiddleware().VerifyFailFunc(c, err)
		return
	}
	// 密码校验逻辑
	if req.Username == "admin" && req.Password == "admin" {
		// 校验通过
		/// 此处模拟向jwt的payload中写入username
		c.SetCookie("jwt-payload", req.Username, 0, "/", "", true, true)
		c.JSON(200, gin.H{"msg": "OK"})
		return
	}
	verify.GetMiddleware().VerifyFailFunc(c, errors.New("verify password err"))
}

func VerifyPassword(c *gin.Context) {
	req := VerifyPasswordReqEnt{}
	// 获取请求参数
	if err := c.ShouldBindJSON(&req); err != nil {
		verify.GetMiddleware().VerifyFailFunc(c, err)
		return
	}
	/// 此处模拟从jwt的payload中取出username
	if username, err := c.Cookie("jwt-payload"); err != nil {
		verify.GetMiddleware().VerifyFailFunc(c, err)
		return
	} else if username == "admin" && req.Password == "admin" {
		// 校验通过
		verify.GetMiddleware().GenerateContext(c)
		c.JSON(200, gin.H{"msg": "OK"})
		return
	}
	verify.GetMiddleware().VerifyFailFunc(c, errors.New("verify password err"))
}

func Hello(c *gin.Context) {
	c.JSON(http.StatusOK, gin.H{"msg": "hello"})
}

func main() {
	app.InitGlobalContext()
	defer app.Shutdown() // 程序退出前触发全局清理

	// 初始化Gin引擎及路由
	r := gin.Default()

	userGroup := r.Group("/user")
	{
		// 登录
		userGroup.POST("/login", Login)
		// 校验密码
		userGroup.POST("/verify", VerifyPassword)
		// 限制资源,必须校验密码后才能访问
		userGroup.GET("/hello", verify.GetMiddleware().CheckContext, Hello)
	}

	// 显式创建http.Server
	server := &http.Server{
		Addr:    ":8080", // 监听地址
		Handler: r,       // 绑定Gin引擎
	}

	// 启动服务器,默认监听8080端口
	// 启动Gin服务(非阻塞)
	go func() {
		// 启动服务
		if err := server.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
			os.Exit(1) // 启动失败时退出程序
		}
	}()

	// 等待全局退出信号(如Ctrl+C、kill)
	<-app.GlobalContext().Done()

	// 关闭Gin服务(等待正在处理的请求完成,超时5秒)
	shutdownCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
	defer cancel()

	// 调用Shutdown,通知服务停止接受新请求并等待现有请求完成
	if err := server.Shutdown(shutdownCtx); err != nil {
		os.Exit(1)
	}
}
go
package app

import (
	"context"
	"os/signal"
	"sync"
	"syscall"
)

var (
	globalCtx    context.Context
	globalCancel context.CancelFunc
	once         sync.Once // 确保初始化只执行一次
)

// InitGlobalContext 初始化全局上下文,应在程序启动时调用一次
func InitGlobalContext() {
	once.Do(func() {
		// 创建可被系统信号取消的根上下文
		globalCtx, globalCancel = signal.NotifyContext(
			context.Background(),
			syscall.SIGINT,  // Ctrl+C
			syscall.SIGTERM, // kill 命令
			syscall.SIGQUIT, // Ctrl+\
		)
	})
}

// GlobalContext 返回全局根上下文,其他包可通过此获取退出信号
func GlobalContext() context.Context {
	if globalCtx == nil {
		// 未初始化根上下文
		panic("global context not initialized")
	}
	return globalCtx
}

// Shutdown 触发全局上下文取消,通知所有依赖退出
func Shutdown() {
	if globalCancel != nil {
		globalCancel() // 触发所有依赖此上下文的组件退出
	}
}
go
package verify

import (
	"container/list"
	"example/app"
	"sync"
	"time"

	uuid "github.com/satori/go.uuid"
)

// TokenManagerImpl 令牌管理结构体
type TokenManagerImpl struct {
	tokens     map[string]*list.List
	expireTime time.Duration
	mutex      sync.RWMutex
	once       sync.Once
}

// Token 验证token结构体
type Token struct {
	Content    string
	ExpireTime time.Time
}

// AddToken 添加Token
func (tm *TokenManagerImpl) AddToken(key string) string {
	tm.mutex.Lock()
	defer tm.mutex.Unlock()

	if _, exists := tm.tokens[key]; !exists {
		tm.tokens[key] = list.New()
	}

	// 使用uuid生成随机uuid作为令牌
	tokenContent := uuid.NewV4().String()
	expireTime := time.Now().Add(tm.GetExpireTime())

	tm.tokens[key].PushBack(Token{
		Content:    tokenContent,
		ExpireTime: expireTime,
	})

	return tokenContent
}

// CheckAndRemoveToken 检查Token有效性并清除
func (tm *TokenManagerImpl) CheckAndRemoveToken(key string, content string) bool {
	tm.mutex.Lock()
	defer tm.mutex.Unlock()
	// 初始化定时清理线程
	tm.once.Do(func() {
		ticker := time.NewTicker(1 * time.Minute)

		go func() {
			defer ticker.Stop() // 退出时停止ticker,释放资源

			for {
				select {
				case <-ticker.C:
					// 执行清理逻辑
					tm.clearExpiredTokens()
				case <-app.GlobalContext().Done():
					// 根上下文结束,退出goroutine
					return
				}
			}
		}()
	})
	tokenList, exists := tm.tokens[key]
	if !exists || tokenList.Len() == 0 {
		return false
	}

	for e := tokenList.Front(); e != nil; e = e.Next() {
		token := e.Value.(Token)
		if token.Content == content {
			tokenList.Remove(e)
			return true
		}
	}
	return false
}

// GetExpireTime 获取过期时间配置
func (tm *TokenManagerImpl) GetExpireTime() time.Duration {
	return tm.expireTime
}

// clearExpiredTokens 清除过期Tokens
func (tm *TokenManagerImpl) clearExpiredTokens() {
	now := time.Now()
	for key := range tm.tokens {
		tokenList := tm.tokens[key]
		for e := tokenList.Front(); e != nil; {
			next := e.Next()
			token := e.Value.(Token)
			if token.ExpireTime.Before(now) {
				tokenList.Remove(e)
			}
			e = next
		}
		// 如果用户列表为空,删除列表
		if tokenList.Len() == 0 {
			delete(tm.tokens, key)
		}
	}
}
go
package verify

import (
	"errors"
	"net/http"
	"sync"
	"time"

	"container/list"

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

var (
	m    *Middleware
	once sync.Once
)

func GetMiddleware() *Middleware {
	once.Do(func() {
		var err error
		m, err = New(&Middleware{
			GetKeyFunc: func(c *gin.Context) (string, error) {
				v, err := c.Cookie("jwt-payload")
				if err != nil {
					return "", errors.New("payload not exist")
				}
				return v, nil
			},
		})
		if err != nil {
			panic(err)
		}
	})
	return m
}

type TokenManager interface {
	// AddToken 指定key生成并返回令牌
	AddToken(key string) string
	// CheckAndRemoveToken 检查对应key的令牌是否有效,并移除过期的令牌
	CheckAndRemoveToken(key string, content string) bool
	// GetExpireTime 获取过期时间
	GetExpireTime() time.Duration
}

type Middleware struct {
	GetKeyFunc      func(c *gin.Context) (string, error)
	VerifyFailFunc  func(c *gin.Context, err error)
	VerifyCookieKey string
	TokenManager    TokenManager
	ExpireTime      time.Duration
}

var (
	ErrUnimplementedGetKeyFunc = errors.New("unimplemented method: GetKeyFunc")
	ErrGetVerifyKey            = errors.New("get verify key fail")
	ErrGetVerifyContext        = errors.New("get verify app fail")
	ErrCheckContext            = errors.New("check fail")
)

// New for check error with Middleware
func New(m *Middleware) (*Middleware, error) {
	if err := m.MiddlewareInit(); err != nil {
		return nil, err
	}

	return m, nil
}

func (vm *Middleware) MiddlewareInit() error {
	if vm.GetKeyFunc == nil {
		return ErrUnimplementedGetKeyFunc
	}
	if vm.ExpireTime == 0 {
		vm.ExpireTime = 5 * time.Minute
	}
	if vm.TokenManager == nil {
		vm.TokenManager = &TokenManagerImpl{
			tokens:     make(map[string]*list.List),
			expireTime: vm.ExpireTime,
		}
	}
	if vm.VerifyFailFunc == nil {
		vm.VerifyFailFunc = func(c *gin.Context, err error) {
			c.JSON(http.StatusInternalServerError, gin.H{
				"msg": err.Error(),
			})
			c.Abort()
		}
	}
	if vm.VerifyCookieKey == "" {
		vm.VerifyCookieKey = "verify-token"
	}
	return nil
}

func (vm *Middleware) CheckContext(c *gin.Context) {
	// 获取用户身份信息
	key, err := vm.GetKeyFunc(c)
	if err != nil {
		vm.VerifyFailFunc(c, ErrGetVerifyKey)
		return
	}
	// 获取verify参数
	verify, err := c.Cookie(vm.VerifyCookieKey)
	if err != nil {
		vm.VerifyFailFunc(c, ErrGetVerifyContext)
		return
	}
	// 清除Cookie
	c.SetCookie(vm.VerifyCookieKey, "", -1, "/", "", true, true)
	// 验证
	if vm.TokenManager.CheckAndRemoveToken(key, verify) {
		// 检查通过
		c.Next()
	} else {
		vm.VerifyFailFunc(c, ErrCheckContext)
		return
	}
}

func (vm *Middleware) GenerateContext(c *gin.Context) {
	// 获取用户身份信息
	key, err := vm.GetKeyFunc(c)
	if err != nil {
		vm.VerifyFailFunc(c, ErrGetVerifyKey)
		return
	}
	// 生成令牌
	verifyToken := vm.TokenManager.AddToken(key)
	c.SetCookie(vm.VerifyCookieKey, verifyToken, 0, "/", "", true, true)
}