感谢分享
Go 中间件设计:织入日志与鉴权时不破坏核心逻辑
在 Go 服务里,中间件最容易被写成“顺手加一段”的地方:一开始只是在 handler 里打一行日志,后来加上 token 校验,再后来每个接口都复制一遍。结果是核心业务逻辑被日志、鉴权、限流、恢复等横切关注点层层包裹,读起来费劲,改起来更危险。中间件的价值,恰恰是把这些通用逻辑从核心 handler 中抽离出来,用组合的方式织入,而不是让业务代码为它们让路。
中间件的本质:包装 http.Handler
Go 标准库的 `net/http` 给了非常朴素的中间件模型:一个函数接收 `http.Handler`,返回一个新的 `http.Handler`。
type Middleware func(http.Handler) http.Handler
func Logging(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
next.ServeHTTP(w, r)
log.Printf("%s %s %v", r.Method, r.URL.Path, time.Since(start))
})
}
关键点在于:中间件只认识 `next`,不关心 `next` 里面是订单逻辑还是用户逻辑。组合时像洋葱一样层层包裹:
mux := http.NewServeMux()
mux.HandleFunc("/orders", orderHandler)
handler := Auth(Logging(mux))
http.ListenAndServe(":8080", handler)
顺序就是执行顺序。外层先进入,内层后进入,返回时反过来。把日志放在鉴权外层,鉴权失败的请求也能被记录;把恢复放在最外层,任何 panic 都不会拖垮整个服务。
鉴权:只做身份判断,不掺业务规则
鉴权中间件应该只回答一个问题:这个请求是谁,是否合法。它负责解析 token、校验签名、把用户信息注入 `context`,失败就返回 401 或 403,成功则调用 `next`。
func Auth(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
token := r.Header.Get("Authorization")
user, err := verifyToken(token)
if err != nil {
http.Error(w, "unauthorized", http.StatusUnauthorized)
return
}
ctx := context.WithValue(r.Context(), userKey{}, user)
next.ServeHTTP(w, r.WithContext(ctx))
})
}
核心 handler 只从 context 取用户,不关心 token 长什么样、怎么解析:
func orderHandler(w http.ResponseWriter, r *http.Request) {
user := r.Context().Value(userKey{}).(*User)
// 只处理订单逻辑
}
“这个用户能不能买这个商品”“这个订单是否属于当前用户”属于业务权限,不应塞进通用鉴权中间件。否则中间件会依赖具体业务模型,最终变成另一个难维护的 handler。
日志:记录事实,别成为业务负担
日志中间件也不该侵入业务。它应该统一记录方法、路径、状态码、耗时、请求 ID,而不是让每个 handler 自己写开始和结束日志。为了拿到状态码,可以包装 `ResponseWriter`:
type statusRecorder struct {
http.ResponseWriter
status int
bytes int
}
func (r *statusRecorder) WriteHeader(code int) {
r.status = code
r.ResponseWriter.WriteHeader(code)
}
func (r *statusRecorder) Write(b []byte) (int, error) {
if r.status == 0 {
r.status = http.StatusOK
}
n, err := r.ResponseWriter.Write(b)
r.bytes += n
return n, err
}
然后在日志中间件里使用它:
func Logging(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
rec := &statusRecorder{ResponseWriter: w}
next.ServeHTTP(rec, r)
log.Printf("method=%s path=%s status=%d bytes=%d dur=%v",
r.Method, r.URL.Path, rec.status, rec.bytes, time.Since(start))
})
}
同时要记得脱敏:不要打印 `Authorization`、密码、完整身份证号。日志写入尽量异步或走高性能 logger,避免阻塞请求。
组合顺序与边界
一个常见的组合顺序是:`Recovery -> RequestID -> Logging -> Auth -> RateLimit -> Business`。Recovery 最外层兜底,RequestID 为后续日志提供关联 ID,Logging 记录鉴权失败,Auth 拒绝非法请求,RateLimit 控制流量,最后才进入业务 handler。
边界要清晰:中间件处理“所有请求都该做的事”,handler 处理“这个请求独有的业务”。不要用全局变量传递用户,不要在中间件里查订单、扣库存、发消息。中间件保持小、单一职责、可独立测试,核心逻辑才能保持干净。
测试与演进
中间件本身可以用 `httptest` 独立测试:构造请求,看 `next` 是否被调用、context 是否正确、状态码是否符合预期。核心 handler 测试时,不需要启动鉴权中间件,直接构造带 user 的 context 即可。这就是解耦带来的好处:日志归日志,鉴权归鉴权,业务归业务,各自演进,互不绑架。
中间件不是把代码藏起来,而是把横切关注点从核心逻辑中抽离出来。织入日志与鉴权时,保持统一签名、单一职责、context 传值、洋葱组合,核心 handler 就能继续只关心业务本身。代码越干净,测试越简单,演进也越从容。
转载请注明出处,版权归原作者所有。
管理员
黑卡会员
正式会员