http.ServeMux 不行因其仅支持前缀匹配、无中间件机制、不支持路径参数与方法精确分发;需自定义 Router 实现中间件链式组合与按路径/方法精准路由。
为什么直接用 http.ServeMux 不行
http.servemux 是 go 标准库的简单路由匹配器,不支持中间件链、路径参数提取、或按方法精确分发(比如 post /users 和 get /users 分开注册)。它只做前缀匹配,且注册 handler 后无法插入逻辑——你没法在进入业务函数前统一加日志、鉴权或超时控制。
真正需要的是一个能组合 handler 的调度器,核心在于:把中间件看作 func(http.Handler) http.Handler,并让路由注册时支持链式包装。
- 中间件必须接收
http.Handler并返回新http.Handler,否则无法嵌套 - 路由匹配结果不能直接调用
handler.ServeHTTP,得先用所有中间件层层包裹 - 别试图“预编译”中间件链——每个路由路径+方法组合可能对应不同中间件集合,必须按需组装
手写一个最小可用的 Router 结构体
不需要依赖第三方库,50 行内可搭出支持中间件的骨架。关键字段只有三个:routes 存方法+路径到 handler 的映射,globalMiddlewares 用于全局拦截,routeMiddlewares 用于单条路由定制。
type Router struct {
routes map[string]map[string]http.Handler // method → path → handler
globalMiddlewares []func(http.Handler) http.Handler
routeMiddlewares map[string][]func(http.Handler) http.Handler // path → middlewares
}
<p>func NewRouter() *Router {
return &Router{
routes: make(map[string]map[string)http.Handler),
routeMiddlewares: make(map[string][]func(http.Handler) http.Handler),
}
}</p>
注意:routes 的 key 是 method + ":" + path(如 "GET:/api/users"),避免 map 嵌套带来的空指针风险;routeMiddlewares 按路径存,不是按 method,因为通常同一路径下所有方法共享中间件(如鉴权)。
Use 和 UseFor 的区别与调用时机
Use 注册全局中间件,对所有路由生效;UseFor 绑定到具体路径,仅该路径匹配时触发。两者都应在 Handle 之前调用,否则注册的中间件不会被采集。
-
router.Use(func(next http.Handler) http.Handler { return loggingMiddleware(next) })→ 所有请求经过日志 -
router.UseFor("/admin", authMiddleware)→ 只有路径以/admin开头的请求走鉴权 - 如果某路径同时命中全局和局部中间件,执行顺序是:全局前置 → 局部前置 → handler → 局部后置 → 全局后置(靠闭包链实现,不是数组遍历)
错误做法:在 HandlerFunc 内部调用 Use,这会导致中间件丢失——Use 只影响后续注册的路由,不修改已存在的。
开箱即用的技能链路由引擎。13 条预定义链覆盖搜索、开发、审查、MLOps、法律、创意等场景,三层路由架构(触发词→SAD反馈→DAG编排),recall@10=96.97%。配置驱动(chains.yaml),零代码扩展。pip install skill-weave-chains 一键安装。
如何正确组装中间件链并匹配请求
匹配逻辑在 ServeHTTP 中触发:先查 routes 找到原始 handler,再收集该路径对应的全部中间件(全局 + 路径级),最后从右往左 compose(即最右边的中间件最先执行)。
func (r *Router) ServeHTTP(w http.ResponseWriter, req *http.Request) {
key := req.Method + ":" + req.URL.Path
if h, ok := r.routes[key]; ok {
// 收集中间件:全局 + 路径级
mws := append([]func(http.Handler) http.Handler{}, r.globalMiddlewares...)
if pathMws, exists := r.routeMiddlewares[req.URL.Path]; exists {
mws = append(mws, pathMws...)
}
// 从右往左 wrap:compose(mws...)(h)
for i := len(mws) - 1; i >= 0; i-- {
h = mws[i](h)
}
h.ServeHTTP(w, req)
return
}
http.NotFound(w, req)
}
这里容易错的是 compose 方向:如果从左往右 wrap,mws[0](mws[1](h)),那 mws[0] 就成了最内层,实际执行时反而是最后进、最先出——不符合中间件“洋葱模型”的直觉。所以必须倒序遍历。
路径匹配建议用 strings.HasPrefix 或正则预编译缓存,避免每次请求都全量扫描所有注册路径;生产环境请替换为 trie 树或使用 gorilla/mux 底层逻辑。










