go 的 http.handler 天然支持责任链模式,无需自建 chain 结构;核心是闭包嵌套包装,通过 next.servehttp 控制流转,用 context.context 安全传递数据,组装顺序决定执行顺序。

Go 的 http.Handler 本身就是责任链,别自己造轮子
Go 标准库的 http.ServeMux 和中间件组合方式,天然支持责任链。你不需要手写 Next() 或抽象 Chain 结构——那是 Java/Python 思维惯性带来的冗余。
真正该关注的是:怎么让多个 http.Handler 按顺序执行、共享上下文、可控中断。核心就靠闭包 + 嵌套包装。
- 每个中间件接收一个
http.Handler,返回一个新的http.Handler - 内部调用
next.ServeHTTP(w, r)表示“交给下一个处理”,不调就是中断 - 所有中间件必须在注册路由前完成链式组装,顺序决定执行顺序
写一个带日志和鉴权的 Handler 链,三步搞定
典型场景:请求进来先打日志,再检查 token,最后进业务逻辑。错误时提前返回,不往下传。
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
func logging(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
log.Printf("START %s %s", r.Method, r.URL.Path)
next.ServeHTTP(w, r)
log.Printf("END %s %s", r.Method, r.URL.Path)
})
}
func auth(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
token := r.Header.Get("Authorization")
if token == "" {
http.Error(w, "missing token", http.StatusUnauthorized)
return // 中断,不调 next
}
next.ServeHTTP(w, r)
})
}
func homeHandler(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("ok"))
}
// 组装:auth → logging → homeHandler
mux := http.NewServeMux()
mux.Handle("/", auth(logging(http.HandlerFunc(homeHandler))))
- 注意顺序:
auth(logging(...))表示 auth 在最外层,最先执行 - 每个中间件都必须用
http.HandlerFunc包一层,否则类型不匹配 - 不要在中间件里修改
r.URL.Path后直接调next.ServeHTTP—— 多数 Handler 依赖原始路径,改了可能路由失效
为什么不用第三方链式库(比如 alice)?
像 github.com/justinas/alice 这类库,只是把嵌套调用语法糖化成 alice.New(a,b,c).Then(h),底层仍是闭包嵌套。它没解决任何 Go 原生做不到的问题,反而引入额外依赖和隐式行为。
-
alice的Then实际还是构造了一个嵌套Handler,和手写a(b(c(h)))生成的结构完全一致 - 调试时堆栈更难读:你会看到一堆
alice.(*Chain).Then.func1,不如直观看auth、logging清晰 - 如果某个中间件需要访问上一个中间件设置的值(比如把用户 ID 放到
r.Context()),你依然得自己处理 key 冲突和类型断言,库不帮你做
Context 传递数据比全局变量或 struct 字段更安全
责任链中跨中间件传参,别用自定义 struct 套娃,也别用包级变量——并发下会错乱。Go 的 context.Context 是唯一正解。
// 定义 context key(用 unexported struct 避免冲突)
type ctxKeyUser struct{}
func userFromToken(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
uid := parseUID(r.Header.Get("Authorization")) // 假设解析出 int64
ctx := context.WithValue(r.Context(), ctxKeyUser{}, uid)
r = r.WithContext(ctx)
next.ServeHTTP(w, r)
})
}
func adminOnly(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
uid, ok := r.Context().Value(ctxKeyUser{}).(int64)
if !ok || !isAdmin(uid) {
http.Error(w, "admin only", http.StatusForbidden)
return
}
next.ServeHTTP(w, r)
})
}
- key 必须是 unexported 类型(如
struct{}),防止外部包误用同一 key 覆盖值 - 每次修改 context 都要重新赋值
r = r.WithContext(...),原*http.Request不可变 - 不要把大对象塞进 context——只放轻量元数据,比如 ID、traceID、超时控制
return 就直接调 next.ServeHTTP,导致错误响应后还继续执行下游逻辑。比如鉴权失败写了 http.Error,但没 return,业务 handler 仍会运行。这个坑比设计模式本身更常踩。golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!










