本文档说明 middleware.Cache、middleware.CacheIf 与 middleware.ETag 的当前用法。
说明:这些缓存中间件返回原始
gin.HandlerFunc。在
github.com/darkit/gin中,推荐两种接法:
- 全局或分组:
e.Use(...)/r.Use(...)- 单路由:
engine.WrapMiddleware(middleware.Cache(...))
- ✅ 响应缓存:自动缓存 HTTP 响应
- ✅ 条件缓存:根据自定义条件决定是否缓存
- ✅ ETag 支持:自动生成 ETag 并处理 304 Not Modified
- ✅ 自定义缓存键:灵活的缓存键生成策略
- ✅ Cache-Control 头:自动设置缓存控制头
- ✅ Vary 头:支持内容协商,并按请求 Header 值隔离缓存键
- ✅ 分布式缓存:支持外部缓存存储(如 Redis)
- ✅ 安全默认值:默认跳过带
Authorization/Cookie的请求,避免私有响应串用户缓存 - ✅ 回归测试:覆盖命中、过期、Vary、认证跳过与私有响应不缓存
middleware/cache.go- 缓存中间件核心实现middleware/etag.go- ETag 中间件实现middleware/cache_test.go- 单元测试与安全回归测试
examples/cache-demo/main.go- 使用示例和交互式测试页面
import (
"time"
"github.com/darkit/gin/middleware"
)
// 全局/分组级缓存
e.Use(middleware.Cache(5 * time.Minute))// 当没有 nocache 参数时才缓存
e.Use(middleware.CacheIf(func(c *gin.Context) bool {
return c.Query("nocache") == ""
}, 10*time.Minute))// 自动生成 ETag 并处理 304 响应
e.Use(middleware.ETag())// 公开资源可按业务维度生成缓存键。
// 私有用户数据默认会因 Authorization/Cookie 跳过缓存,不建议仅靠 user_id 拼 key 后全局放行。
e.Use(middleware.Cache(5*time.Minute,
middleware.WithCacheKey(func(c *gin.Context) string {
return "public-article:" + c.Param("id")
}),
))e.Use(middleware.Cache(time.Minute,
middleware.WithCacheControl("public, max-age=60"),
middleware.WithCacheVary("Accept-Language", "Accept-Encoding"),
))// 使用 Fiber storage 兼容后端作为缓存存储
customStore := cache.NewFiberStorage(raw)
e.Use(middleware.Cache(time.Minute,
middleware.WithCacheStore(customStore),
))func Cache(duration time.Duration, opts ...CacheOption) gin.HandlerFunc缓存 HTTP 响应指定时间。
参数:
duration- 缓存持续时间opts- 可选的缓存选项
行为:
- 只缓存 GET 和 HEAD 请求
- 默认跳过带
Authorization/Cookie的请求 - 默认跳过请求侧
Cache-Control: no-cache/no-store或Pragma: no-cache - 只缓存成功响应(2xx 状态码)
- 不缓存带
Set-Cookie的响应 - 不缓存响应侧
Cache-Control: private/no-cache/no-store - 自动设置
X-Cache头(HIT 或 MISS)
func CacheIf(condition func(*gin.Context) bool, duration time.Duration, opts ...CacheOption) gin.HandlerFunc根据条件决定是否缓存响应。
参数:
condition- 条件函数,返回 true 时缓存duration- 缓存持续时间opts- 可选的缓存选项
func ETag() gin.HandlerFunc自动生成 ETag 并处理 If-None-Match 请求头。
行为:
- 使用 MD5 哈希计算响应内容的 ETag
- 检查客户端的 If-None-Match 头
- 如果 ETag 匹配,返回 304 Not Modified
- 只处理 GET 和 HEAD 请求
- 只处理成功响应(2xx 状态码)
func WithCacheStore(store cache.Cache) CacheOption设置自定义缓存存储(用于分布式缓存)。若使用 Fiber storage 生态后端,可直接传入:
middleware.WithCacheStore(cache.NewFiberStorage(raw))func WithCacheKey(keyFunc func(*gin.Context) string) CacheOption设置自定义缓存键生成函数。
默认键格式: method:path:querystring 的 SHA256 哈希
func WithCacheControl(control string) CacheOption设置 Cache-Control 响应头。
示例: "public, max-age=60"
func WithCacheVary(headers ...string) CacheOption设置 Vary 响应头,支持内容协商;对应请求 Header 值会纳入缓存键,避免不同语言、编码或内容协商维度共用同一响应。
示例: "Accept-Language", "Accept-Encoding"
func WithCacheSkip(skip func(*gin.Context) bool) CacheOption设置跳过缓存的判断函数。返回 true 时本次请求完全绕过读取与写入缓存。
type cachedResponse struct {
Status int // HTTP 状态码
Headers http.Header // 响应头
Body []byte // 响应体
}缓存会保存完整的响应信息,包括状态码、所有响应头和响应体。
默认情况下,缓存键基于以下信息生成:
SHA256(method:path:querystring)
如果配置 WithCacheVary(...),请求侧对应 Header 值会追加进入缓存键。
例如:
GET /articles/123→cache:a1b2c3...GET /articles/123?lang=en→cache:d4e5f6...
- 检查请求方法(只缓存 GET/HEAD)
- 生成缓存键
- 尝试从缓存获取响应
- 如果缓存命中:
- 反序列化响应
- 设置响应头
- 返回缓存的内容
- 设置
X-Cache: HIT
- 如果缓存未命中:
- 使用自定义 ResponseWriter 拦截响应
- 设置
X-Cache: MISS - 继续处理请求
- 如果响应成功(2xx),保存到缓存
- 检查请求方法(只处理 GET/HEAD)
- 使用自定义 ResponseWriter 拦截响应
- 继续处理请求
- 计算响应内容的 MD5 哈希作为 ETag
- 检查客户端的 If-None-Match 头
- 如果 ETag 匹配:
- 返回 304 Not Modified
- 不返回响应体
- 如果 ETag 不匹配或客户端未提供:
- 设置 ETag 头
- 返回完整响应
覆盖以下核心测试用例:
TestCache_Hit- 缓存命中TestCache_Miss- 缓存未命中TestCache_Expiry- 缓存过期TestCacheIf_Condition- 条件缓存TestCache_CustomKey- 自定义缓存键TestCache_OnlyGetHead- 只缓存 GET/HEAD 请求TestCache_OnlySuccessResponses- 只缓存成功响应TestCache_WithCacheControl- Cache-Control 头TestCache_WithVary- Vary 头TestCache_WithCustomStore- 自定义存储TestCache_SkipsAuthenticatedRequestsByDefault- 默认跳过认证态请求TestCache_VaryRequestHeadersPartitionCacheKey- Vary 请求 Header 隔离缓存键TestCache_DoesNotStorePrivateOrCookieResponses- 私有响应与 Set-Cookie 不缓存TestCache_WithSkipFunc- 自定义跳过函数
TestETag_NotModified- 304 Not Modified 响应TestETag_Modified- ETag 不匹配时返回完整响应TestETag_OnlyGetHead- 只处理 GET/HEAD 请求TestETag_OnlySuccessResponses- 只对成功响应生成 ETagTestETag_DifferentContent- 不同内容生成不同的 ETag
# 运行所有缓存相关测试
go test -v -run "TestCache|TestETag" ./middleware/
# 查看测试覆盖率
go test -coverprofile=coverage.out ./middleware/cache.go ./middleware/etag.go ./middleware/cache_test.go
go tool cover -func=coverage.out
# 运行示例
go run examples/cache-demo/main.go-
选择合适的缓存时间
- 静态内容:较长时间(如 1 小时)
- 动态内容:较短时间(如 1-5 分钟)
- 实时数据:不缓存或使用 ETag
-
使用分布式缓存
- 对于多实例部署,可自定义实现
pkg/cache.Cache后注入 - 当前仓库未内置 Redis 实现
- 避免每个实例维护独立缓存
- 对于多实例部署,可自定义实现
-
自定义缓存键
- 对于需要按用户缓存的接口,使用自定义键
- 避免查询参数顺序影响缓存
-
结合 ETag 使用
- 对于变化不频繁的内容,使用 ETag
- 可以减少带宽消耗
-
合理使用条件缓存
- 允许用户绕过缓存(如
nocache参数) - 对管理员或特定用户禁用缓存
- 允许用户绕过缓存(如
-
不要缓存敏感数据
- 用户个人信息应该使用自定义键
- 认证相关接口不应缓存
-
缓存失效策略
- 默认使用 TTL 过期
- 如需主动清除,需要访问底层缓存存储
-
响应头处理
- 缓存会保存所有响应头
Set-Cookie、private、no-cache、no-store响应默认不缓存
-
内存使用
- 中间件默认使用无后台清理 goroutine 的内存缓存
- 大规模应用应使用外部缓存存储
-
并发安全
- 所有中间件都是并发安全的
- 使用了适当的互斥锁保护
// 公开 API,缓存时间较长
e.GET(
"/api/v1/products",
engine.WrapMiddleware(middleware.Cache(10*time.Minute,
middleware.WithCacheControl("public, max-age=600"),
)),
getProducts,
)
// 用户相关 API 默认会因 Authorization/Cookie 跳过缓存。
// 如确需缓存,必须显式确认命名空间与失效策略,例如按 tenant/user/role 生成 key,
// 并使用 WithCacheSkip 放行经过审计的安全场景。// 使用 ETag 处理静态资源
e.GET("/static/*filepath", engine.WrapMiddleware(middleware.ETag()), serveStatic)// 搜索结果缓存,但允许用户强制刷新
e.GET(
"/search",
engine.WrapMiddleware(middleware.CacheIf(func(c *gin.Context) bool {
return c.Query("refresh") != "1"
}, 5*time.Minute)),
searchHandler,
)// 使用 Fiber storage 兼容后端确保多实例缓存一致
customStore := cache.NewFiberStorage(raw)
e.Use(middleware.Cache(time.Minute,
middleware.WithCacheStore(customStore),
))当前缓存中间件提供了完整的响应缓存解决方案,包括:
- ✅ 灵活的缓存策略
- ✅ 条件缓存支持
- ✅ ETag 和 304 处理
- ✅ 自定义缓存键
- ✅ 分布式缓存支持
- ✅ 安全默认值与回归测试
- ✅ 生产级代码质量
所有代码都遵循项目规范,使用中文注释,并提供了详细的使用示例和测试用例。