news 2026/8/13 22:50:09

Golang HTTP 中间件机制

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Golang HTTP 中间件机制

HTTP 中间件机制

一、知识点总结

1.1 什么是中间件(Middleware)

中间件是 HTTP 处理流程中的可插拔处理单元,它在请求到达最终 Handler 之前执行预处理,在 Handler 返回响应之后执行后处理。典型中间件包括:日志记录、认证鉴权、请求限流、panic 恢复、CORS 跨域、请求计时等。

中间件的核心理念可以用一个公式概括:

Middleware(Handler) → NewHandler

中间件接收一个 Handler返回一个新的 Handler。这个新 Handler 在内部调用原始 Handler,但在调用前后插入额外的逻辑。这种模式在 Go 中称为**装饰器模式(Decorator Pattern)**的函数式实现。

1.2 洋葱模型(Onion Model)

中间件的执行顺序是理解其行为的关键。当多个中间件嵌套时,形成洋葱模型

请求 → MiddlewareA → MiddlewareB → MiddlewareC → Handler 响应 ← MiddlewareA ← MiddlewareB ← MiddlewareC ←

即:请求阶段按注册顺序执行,响应阶段按注册顺序的逆序执行。外层中间件可以包裹内层,形成类似函数调用的栈结构。

1.3 标准库中的中间件实现方式

Go 标准库没有"中间件"这个术语,但提供了实现中间件的所有工具。最常见的两种实现方式:

方式一:Handler 包装(推荐)

funcLoggerMiddleware(next http.Handler)http.Handler{returnhttp.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))})}

方式二:HandlerFunc 包装

funcLoggerMiddlewareFunc(next http.HandlerFunc)http.HandlerFunc{returnfunc(w http.ResponseWriter,r*http.Request){start:=time.Now()next(w,r)log.Printf("%s %s %v",r.Method,r.URL.Path,time.Since(start))}}

方式一更通用(接受http.Handler),方式二更便捷(与HandleFunc配合)。实际项目中推荐使用方式一,因为它更标准、可复用性更强。

1.4 关键技巧:ResponseWriter 包装

很多中间件需要读取或修改响应数据(如日志记录响应状态码、Gzip 压缩响应体)。但ResponseWriter接口没有提供读取已写入内容的方法。解决方法是创建自定义的 ResponseWriter 包装器

typeresponseRecorderstruct{http.ResponseWriter statusintsizeint}func(rr*responseRecorder)WriteHeader(statusint){rr.status=status rr.ResponseWriter.WriteHeader(status)}func(rr*responseRecorder)Write(b[]byte)(int,error){ifrr.status==0{rr.status=http.StatusOK}n,err:=rr.ResponseWriter.Write(b)rr.size+=nreturnn,err}

这个包装器内嵌http.ResponseWriter,重写了WriteHeaderWrite方法以捕获状态码和写入字节数。这是 Go 中间件开发中最核心的技巧之一。

1.5 Panic 恢复中间件

生产环境必须有一个 recover 中间件,防止单个请求的 panic 导致整个服务崩溃:

funcRecoverMiddleware(next http.Handler)http.Handler{returnhttp.HandlerFunc(func(w http.ResponseWriter,r*http.Request){deferfunc(){ifrec:=recover();rec!=nil{log.Printf("Panic recovered: %v",rec)http.Error(w,"Internal Server Error",500)}}()next.ServeHTTP(w,r)})}

1.6 链式中间件组合

当有多个中间件时,可以用一个辅助函数把它们串联起来:

funcChain(h http.Handler,middlewares...func(http.Handler)http.Handler)http.Handler{fori:=len(middlewares)-1;i>=0;i--{h=middlewares[i](h)}returnh}// 使用handler:=Chain(myHandler,Logger,Recover,Auth)

注意循环是从后往前遍历的——这样 Middleware1 → Middleware2 → Middleware3 的注册顺序,实际执行时就是 Middleware1 在最外层包裹,Middleware3 在最内层包裹,符合直觉。

1.7 中间件 vs 框架中间件

特性标准库手写Gin/Echo 框架
接口Handler → Handlerc *gin.Context
参数传递通过 context 或闭包通过 Context 对象
中断请求直接 return,不调用 nextc.Abort()
链式中断手动控制内置 Abort 机制
性能开销零额外开销有 Context 对象创建开销

二、练习代码

示例 1:日志记录中间件

packagemainimport("fmt""log""net/http""time")// loggingResponseWriter 包装 http.ResponseWriter 以捕获状态码typeloggingResponseWriterstruct{http.ResponseWriter statusintsizeint}func(lrw*loggingResponseWriter)WriteHeader(statusint){lrw.status=status lrw.ResponseWriter.WriteHeader(status)}func(lrw*loggingResponseWriter)Write(b[]byte)(int,error){iflrw.status==0{lrw.status=http.StatusOK}n,err:=lrw.ResponseWriter.Write(b)lrw.size+=nreturnn,err}// LoggerMiddleware 记录每个请求的耗时、状态码和响应大小funcLoggerMiddleware(next http.Handler)http.Handler{returnhttp.HandlerFunc(func(w http.ResponseWriter,r*http.Request){start:=time.Now()lrw:=&loggingResponseWriter{ResponseWriter:w,status:0}next.ServeHTTP(lrw,r)duration:=time.Since(start)log.Printf("[%s] %s %s | Status: %d | Size: %d bytes | Duration: %v",start.Format("2006-01-02 15:04:05"),r.Method,r.URL.Path,lrw.status,lrw.size,duration,)})}// APIHandler 模拟业务 HandlerfuncAPIHandler(w http.ResponseWriter,r*http.Request){// 模拟业务处理耗时time.Sleep(10*time.Millisecond)w.Header().Set("Content-Type","application/json")fmt.Fprintln(w,`{"status":"ok","data":"hello"}`)}funcErrorHandler(w http.ResponseWriter,r*http.Request){w.WriteHeader(http.StatusInternalServerError)fmt.Fprintln(w,`{"error":"something went wrong"}`)}funcmain(){mux:=http.NewServeMux()mux.HandleFunc("/api/data",APIHandler)mux.HandleFunc("/api/error",ErrorHandler)mux.HandleFunc("/",func(w http.ResponseWriter,r*http.Request){fmt.Fprintln(w,"Welcome! Try /api/data or /api/error")})// 用中间件包装整个 muxvarhandler http.Handler=mux handler=LoggerMiddleware(handler)log.Println("Server on :8080")log.Fatal(http.ListenAndServe(":8080",handler))}

示例 2:Panic 恢复中间件

packagemainimport("fmt""log""net/http""runtime/debug")// RecoverMiddleware 捕获 panic 防止服务崩溃funcRecoverMiddleware(next http.Handler)http.Handler{returnhttp.HandlerFunc(func(w http.ResponseWriter,r*http.Request){deferfunc(){ifrec:=recover();rec!=nil{stack:=debug.Stack()log.Printf("[PANIC RECOVERED] %v\n%s",rec,stack)http.Error(w,"Internal Server Error",http.StatusInternalServerError)}}()next.ServeHTTP(w,r)})}funcmain(){mux:=http.NewServeMux()// 模拟一个会 panic 的 Handlermux.HandleFunc("/panic",func(w http.ResponseWriter,r*http.Request){panic("Oops! Something terrible happened")})mux.HandleFunc("/safe",func(w http.ResponseWriter,r*http.Request){fmt.Fprintln(w,"This endpoint is safe")})// 先应用 Recover,再应用 Loggervarhandler http.Handler=mux handler=RecoverMiddleware(handler)log.Println("Server on :8080 (try /panic and /safe)")log.Fatal(http.ListenAndServe(":8080",handler))}

示例 3:认证中间件 + 链式组合

packagemainimport("fmt""log""net/http""time")// ======== 中间件定义 ========// LoggerMiddleware 记录请求日志funcLoggerMiddleware(next http.Handler)http.Handler{returnhttp.HandlerFunc(func(w http.ResponseWriter,r*http.Request){start:=time.Now()next.ServeHTTP(w,r)log.Printf("[%s] %s %s - %v",r.Method,r.URL.Path,r.UserAgent(),time.Since(start))})}// AuthMiddleware 简单的 Token 认证中间件funcAuthMiddleware(next http.Handler)http.Handler{returnhttp.HandlerFunc(func(w http.ResponseWriter,r*http.Request){// 公开接口不需要认证ifr.URL.Path=="/public"||r.URL.Path=="/"{next.ServeHTTP(w,r)return}// 从 Header 读取 Tokentoken:=r.Header.Get("Authorization")iftoken==""{w.Header().Set("WWW-Authenticate","Bearer")http.Error(w,"Unauthorized: missing token",http.StatusUnauthorized)return}iftoken!="Bearer secret-token-123"{http.Error(w,"Unauthorized: invalid token",http.StatusUnauthorized)return}// 认证通过,继续执行next.ServeHTTP(w,r)})}// CORSMiddleware 简单的跨域中间件funcCORSMiddleware(next http.Handler)http.Handler{returnhttp.HandlerFunc(func(w http.ResponseWriter,r*http.Request){w.Header().Set("Access-Control-Allow-Origin","*")w.Header().Set("Access-Control-Allow-Methods","GET, POST, PUT, DELETE, OPTIONS")w.Header().Set("Access-Control-Allow-Headers","Content-Type, Authorization")// 处理预检请求ifr.Method==http.MethodOptions{w.WriteHeader(http.StatusOK)return}next.ServeHTTP(w,r)})}// RecoverMiddleware panic 恢复funcRecoverMiddleware(next http.Handler)http.Handler{returnhttp.HandlerFunc(func(w http.ResponseWriter,r*http.Request){deferfunc(){ifrec:=recover();rec!=nil{log.Printf("[PANIC] %v",rec)http.Error(w,"Internal Server Error",http.StatusInternalServerError)}}()next.ServeHTTP(w,r)})}// ======== 链式组合工具 ========// Chain 将多个中间件按顺序串联// 注意:从后往前遍历,保证 middlewares[0] 在最外层funcChain(h http.Handler,middlewares...func(http.Handler)http.Handler)http.Handler{fori:=len(middlewares)-1;i>=0;i--{h=middlewares[i](h)}returnh}// ======== Handlers ========funcmain(){mux:=http.NewServeMux()mux.HandleFunc("/",func(w http.ResponseWriter,r*http.Request){fmt.Fprintln(w,"Public endpoint: /public, Protected: /api/data")})mux.HandleFunc("/public",func(w http.ResponseWriter,r*http.Request){fmt.Fprintln(w,`{"message":"This is a public endpoint"}`)})mux.HandleFunc("/api/data",func(w http.ResponseWriter,r*http.Request){fmt.Fprintln(w,`{"message":"Protected data","user":"admin"}`)})// 链式组合中间件(从外到内:CORS -> Logger -> Recover -> Auth -> mux)// 执行顺序:// 1. CORS 预处理 -> 2. Logger 记录开始 -> 3. Recover 捕获// -> 4. Auth 验证 -> 5. Handler 执行// <- 5. Handler 返回 <- 4. Auth 返回 <- 3. Recover 返回 <- 2. Logger 记录结束 <- 1. CORS 后处理handler:=Chain(mux,CORSMiddleware,LoggerMiddleware,RecoverMiddleware,AuthMiddleware,)log.Println("Server on :8080")log.Println(" Public: GET http://localhost:8080/public")log.Println(" Protected: GET http://localhost:8080/api/data")log.Println(" Auth Header: Authorization: Bearer secret-token-123")log.Fatal(http.ListenAndServe(":8080",handler))}

三、今日思考题

  1. 为什么Chain函数要从后往前遍历中间件数组?如果从前向后遍历会有什么问题?
  2. loggingResponseWriter为什么要内嵌http.ResponseWriter而不是直接实现完整的http.ResponseWriter接口?(提示:http.ResponseWriter 还包含哪些方法?)
  3. 中间件中的 panic recovery 为什么一定要放在defer中?如果直接写recover()会怎样?

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/8/13 22:48:06

Vue3可扩展Provider系统设计:IoC容器与依赖注入在AI平台的应用

1. 项目概述&#xff1a;为什么我们需要一个可扩展的 Provider 系统&#xff1f;在构建现代前端应用&#xff0c;尤其是基于 Vue3 的复杂中后台系统时&#xff0c;我们常常会遇到一个核心挑战&#xff1a;如何优雅地管理那些分散在应用各个角落的、与外部服务或复杂内部状态打交…

作者头像 李华
网站建设 2026/8/13 22:43:02

Linux系统运维:使用ps命令深入排查多线程应用性能问题

1. 从一次线上故障排查说起&#xff1a;为什么只看进程不够 那天下午&#xff0c;监控系统突然报警&#xff0c;提示某个核心服务的CPU使用率飙升到200%以上&#xff0c;但内存和网络IO都还正常。我第一反应是登录服务器&#xff0c;用最熟悉的 top 命令看了一眼&#xff0c;…

作者头像 李华
网站建设 2026/8/13 22:38:46

服装自产自销公司选软件别买错:九个避坑要点

做服装自产自销的老板&#xff0c;这几年应该都有同一个感受&#xff1a;生意越来越难做&#xff0c;单子越来越碎&#xff0c;利润越来越薄。以前靠经验、靠人盯、靠Excel硬扛&#xff0c;还能勉强转得动。到了2026年&#xff0c;这条路径已经走不通了。于是很多老板开始考虑上…

作者头像 李华
网站建设 2026/8/13 22:34:01

从零搭建CentOS 7 EDA环境:IC618+SPECTRE18+Calibre2019全流程指南

1. 从零开始的EDA环境构建&#xff1a;为什么是这套组合&#xff1f; 如果你刚踏入模拟集成电路设计的大门&#xff0c;或者从其他领域转过来&#xff0c;面对的第一个硬骨头往往不是电路理论&#xff0c;而是那个传说中的“环境搭建”。论坛里、群里&#xff0c;前辈们总是轻描…

作者头像 李华
网站建设 2026/8/13 22:31:46

MySQL-Innodb-内存结构

一、 Innodb内存结构的基本组成 1.1 基本结构说明Innodb内存结构的组成大致有&#xff1a; Buffer pool&#xff1a;缓冲池&#xff0c;用于存储数据页、索引页&#xff08;包括自适应哈希索引、undolog缓冲区&#xff09;&#xff1b;Change buffer&#xff1a;变更缓冲区&…

作者头像 李华