上一篇把业务用例接到数据库,并明确了事务边界。本篇回到请求链:设计可组合中间件、版本化资源路由、认证授权和限流。目标是让横切能力可观察、可测试,同时避免中间件变成隐藏业务逻辑的杂物间。
一、用函数组合建立请求管道
标准中间件类型可写成func(http.Handler) http.Handler。组合顺序由外向内执行请求、由内向外完成响应,因此 request ID 应早于日志,日志才能记录它;恢复器必须覆盖可能 panic 的范围。把顺序放在一个显式chain函数,而不是散落在注册代码中。
访问日志记录方法、规范化路径模板、状态码、耗时、响应字节和 request ID。不要用原始 URL 作为指标标签,否则/users/1、/users/2会制造无限基数;也不要默认记录 Authorization、Cookie 或完整请求体。状态码需要包装 ResponseWriter 才能捕获,但包装时要留意 Flusher、Hijacker 等可选接口,流式和 WebSocket 场景应使用经过验证的实现。
请求 ID 可接受上游符合格式的值,否则生成新值,并回写响应头。它用于关联,不替代分布式 trace。context key 应用私有类型,避免包间字符串冲突;传入业务层时优先显式参数,只有真正的请求元数据放 context。
下面程序组合 request ID 与日志中间件,并通过内存请求得到确定性结果。
packagemainimport("context""fmt""net/http""net/http/httptest")typecontextKeyintconstrequestIDKey contextKey=1typeMiddlewarefunc(http.Handler)http.Handlerfuncchain(handler http.Handler,items...Middleware)http.Handler{fori:=len(items)-1;i>=0;i--{handler=items[i](handler)}returnhandler}funcrequestID(next http.Handler)http.Handler{returnhttp.HandlerFunc(func(w http.ResponseWriter,r*http.Request){id:=r.Header.Get("X-Request-ID")ifid==""{id="generated-001"}w.Header().Set("X-Request-ID",id)next.ServeHTTP(w,r.WithContext(context.WithValue(r.Context(),requestIDKey,id)))})}funcaccessLog(next http.Handler)http.Handler{returnhttp.HandlerFunc(func(w http.ResponseWriter,r*http.Request){next.ServeHTTP(w,r)fmt.Printf("method=%s path=%s request_id=%s\n",r.Method,r.URL.Path,r.Context().Value(requestIDKey))})}funcmain(){endpoint:=http.HandlerFunc(func(w http.ResponseWriter,r*http.Request){w.WriteHeader(http.StatusNoContent)})handler:=chain(endpoint,requestID,accessLog)request:=httptest.NewRequest(http.MethodGet,"/v1/tasks",nil)response:=httptest.NewRecorder()handler.ServeHTTP(response,request)fmt.Printf("status=%d response_id=%s\n",response.Code,response.Header().Get("X-Request-ID"))}运行输出:
method=GET path=/v1/tasks request_id=generated-001 status=204 response_id=generated-001二、认证、授权、CORS 与限流各司其职
认证回答“你是谁”,授权回答“你能做什么”。中间件可以验证签名、有效期、issuer 和 audience,并得到主体;资源所有权检查通常需要业务数据,应在用例或策略层完成。只验证 JWT 签名却不限制算法、签发者和受众,会接受不属于本服务的 token。密钥轮换应支持 key ID 和短暂重叠。
浏览器 CORS 预检不携带普通业务语义,应明确允许的 origin、method、header 和缓存时间。CORS 不是防止 curl 调用的安全边界。使用 cookie 时还要处理 SameSite、Secure 和 CSRF;使用 Authorization header 可降低部分 CSRF 风险,但 XSS 仍可能窃取 token。
令牌桶允许以固定速率补充令牌并容纳短时突发。单实例内存限流简单快速,但扩容后每实例各有额度;需要全局严格额度时可用 Redis 原子脚本或网关,代价是额外网络依赖。限流键选用户、API key 或可信代理解析后的 IP,不能盲信任任意X-Forwarded-For。
packagemainimport("fmt""sync""time")typeBucketstruct{mu sync.Mutex tokensfloat64capacityfloat64ratefloat64last time.Time}func(b*Bucket)Allow(now time.Time)bool{b.mu.Lock()deferb.mu.Unlock()elapsed:=now.Sub(b.last).Seconds()b.tokens+=elapsed*b.rateifb.tokens>b.capacity{b.tokens=b.capacity}b.last=nowifb.tokens<1{returnfalse}b.tokens--returntrue}funcmain(){start:=time.Unix(0,0)bucket:=&Bucket{tokens:2,capacity:2,rate:1,last:start}fori:=1;i<=3;i++{fmt.Printf("request=%d allowed=%t\n",i,bucket.Allow(start))}fmt.Printf("after_1s allowed=%t\n",bucket.Allow(start.Add(time.Second)))}运行输出:
request=1 allowed=true request=2 allowed=true request=3 allowed=false after_1s allowed=true三、设计长期可演进的路由
路径使用复数资源名,例如/v1/projects/{projectID}/tasks。嵌套表达归属,但不要无限加深;可以为任务提供/v1/tasks/{id}的直接读取路径。查询参数承担过滤、排序与分页,字段名和排序方向走白名单。尾斜杠、大小写和 URL 解码策略要统一测试,避免代理与应用理解不同。
版本放路径最直观,也便于网关路由;放媒体类型更纯粹但运维和调试复杂。只有不兼容契约才升主版本,增加可选字段通常无需新版本。废弃接口应先发布期限和替代方案,观测旧版本调用者,再移除;不能仅凭“文档已更新”判断无人使用。
路由测试应构造表格,覆盖正确方法、错误方法、参数缺失、编码字符、认证失败和 404/405。405 与 404 的区分帮助客户端定位方法错误,但也需考虑是否暴露资源存在性。所有错误响应保持相同 schema 和 content type。
中间件的最佳边界是与具体业务无关、能独立验证的请求横切逻辑。下一篇将为这些组件建立环境配置、结构化日志和统一错误分类,让问题在本地、测试和生产环境中都能被可靠定位。
参考来源
- Go 官方文档:net/http
- OWASP:认证备忘单
- MDN:CORS
- IETF:RateLimit Header Fields
👍 觉得有用就点个赞 + 收藏,方便回头查阅;有疑问直接在评论区留言,我看到都会回。
🚀 本文属于《Go 后端开发实战》系列,持续更新,关注不迷路。
📌 文章里的代码都能直接跑。想要可直接 clone 的完整工程 + 配套部署脚本 / 踩坑清单?评论一声或发邮件到cj2664@qq.com,我免费发你。
如果你正好在做类似系统、或有工程化难题想找人做,也欢迎邮件聊一句——我按实际情况评估,能落地的就接单或出方案。评论和邮件都能直接找到我,不用跳别的平台。