[7天实战入门Go语言后端] Day 5:中间件与业务分层——日志、鉴权与请求超时

本日关键词(实战):中间件、Handler 包装、日志中间件、鉴权、API Key、请求超时、context.WithTimeout、handler→service→repository

本日语法/概念(实战):

语法/概念 实战用途 本日示例
func(next http.Handler) http.Handler 中间件:包装下一层,先做自己的事再调 next Logging、APIKeyAuth、Timeout
r.Header.Get("X-API-Key") 从 Header 取 API Key,做简单鉴权 auth.go
context.WithTimeout(ctx, d) 给请求设超时,下游用 r.Context() 可被取消 context_timeout.go
r.WithContext(ctx) 把带超时的 ctx 放进请求,下一层 r.Context() 即此 ctx 超时中间件
http.StripPrefix("/api", h) 去掉 URL 前缀再交给子 mux,便于挂 /api/hello 等 main.go
handler → service → repository 分层:接口、业务、数据;本日以 handler+中间件为主 Day 7 展开

获取实战代码 :如需在本地跑通本文示例,请克隆仓库 WenSongWang/go-quickstart-7days,本文示例在 day5 目录,克隆后在项目根目录执行下文中的命令即可。


一、本篇目标

学完本文并跑通本目录示例,你将掌握:

模块 内容
中间件 包装 http.Handler:日志、鉴权、请求超时(context)
链式顺序 请求依次经过:日志 → 超时 → 鉴权 → 业务
鉴权 从 Header 读取 X-API-Key,未通过返回 401(示例级)
Context 超时中间件注入带取消的 context,面试常问

二、前置要求

  • 已完成 Day 1~4
  • 命令在项目根目录执行。

三、本日目录结构与示例(先混个眼熟)

目录结构

复制代码
day5/
├── cmd/
│   └── server/
│       └── main.go          # 入口:路由、中间件链、ListenAndServe
├── internal/
│   └── middleware/
│       ├── logging.go       # 日志中间件:记耗时、调 next、打日志
│       ├── auth.go          # 鉴权中间件:校验 X-API-Key,不对则 401
│       └── context_timeout.go  # 超时中间件:WithTimeout、r.WithContext
├── README.md
└── csdn.md
目录/文件 说明
cmd/server/ 可执行入口,一个 main 包;挂路由、拼中间件链、起 8080 服务。
internal/middleware/ 仅本项目内部使用的中间件包,对外不暴露;放 Logging、APIKeyAuth、Timeout 等可复用中间件。
main.go 用 NewServeMux 挂 /health 与 /api/;/api 下用 StripPrefix + 超时→鉴权→api 链,最外层再包 Logging。

示例与知识点

示例 主要知识点
cmd/server/main.go NewServeMux、StripPrefix、中间件链(Logging → Timeout → APIKeyAuth → api)
internal/middleware/logging.go 中间件写法:先记时间,调 next,再打日志
internal/middleware/auth.go 取 X-API-Key,不对则 401 并 return,不调 next
internal/middleware/context_timeout.go WithTimeout、r.WithContext(ctx),下游用 r.Context()

四、核心概念与最小示例(不看代码也能懂)

什么是中间件?为什么用「包装」?

中间件就是在业务 Handler 外面再包一层 :请求先经过你这层(例如打日志、查 API Key、设超时),再决定是否调用「下一层」next.ServeHTTP(w, r)。这样日志、鉴权、超时可以和业务解耦,改一处、全站生效,不用在每个 Handler 里重复写。

中间件为什么有「一层 return」和「两层 return」?

  • Loggingfunc Logging(next http.Handler) http.Handler一层 return ,直接返回一个 Handler;因为 next 已经作为参数传进来了。
  • Timeout / APIKeyAuthfunc Timeout(d) func(http.Handler) http.Handler两层 return :先返回「一个函数」,调用时再传 next 得到 Handler;这样可以在「造中间件」时带入参数(超时时间 d、API Key 字符串),再拿去包装不同的 next。总结:要带参数就用「两层」(返回函数),不带参数就「一层」(直接返回 Handler)。

中间件顺序有什么讲究?

先包装的先执行 。本日顺序是:Logging(mux) 最外层,所以请求先打日志;对 /api/ 再包一层 Timeout(APIKeyAuth(api)),所以进 API 后先超时、再鉴权、再进业务。若把鉴权放在最外层,未带 Key 的请求就不会经过超时和日志(看需求而定)。

请求超时怎么做?为什么用 context?

超时要做两件事:给请求一个「最多执行多久」的 deadline ,以及超时后能让下游(DB、RPC)一起停 。做法是:在中间件里用 context.WithTimeout(r.Context(), 5*time.Second) 得到带超时的 ctx,再用 r.WithContext(ctx) 把新 ctx 放进请求;下一层用 r.Context() 拿到的就是这个 ctx。下游在调 DB/RPC 时把这个 ctx 传进去,超时后 ctx 被取消,下游就能收到取消信号并退出。面试常问:如何做请求超时?答:中间件里 WithTimeout + r.WithContext,业务里用 r.Context()。

鉴权这里为什么只做「示例」?API Key 从哪来?

本日只用 Header 里的 X-API-Key 和固定字符串 "secret" 比对,适合理解「中间件如何拦截请求」。生产环境 :API Key 应从配置(如 Day 3 的 viper/cfg)或环境变量读取,不要写死在代码里。真实项目里还会做 JWT、OAuth、Session 等,但拦截方式一样 :在中间件里校验,不通过就 WriteHeader(401)return,不调用 next延伸阅读接口鉴权综述:签名、AK/SK、加解密与常见方式(附 Python 示例)。

易踩坑小结

原因 解法
中间件里已 401 又调了 next 会再写一次响应,可能重复 WriteHeader 校验不通过时直接 return,不要调 next.ServeHTTP
业务里拿不到「带超时的 ctx」 没用 r.WithContext 传给下一层 超时中间件里必须 next.ServeHTTP(w, r.WithContext(ctx))
顺序搞反 先包装的后执行(洋葱模型) 记清:Logging(mux) 表示请求先经 Logging,再进 mux
API Key 写死 示例里 "secret" 硬编码,上线难改 从配置(Day 3 cfg)或环境变量读取

五、Day 5 示例代码与逐段解读

1. cmd/server/main.go

go 复制代码
package main

import (
	"net/http"
	"time"

	"github.com/go-quickstart-7days/day5/internal/middleware"
)

func main() {
	mux := http.NewServeMux()
	mux.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) {
		w.Write([]byte("ok"))
	})

	api := http.NewServeMux()
	api.HandleFunc("/hello", func(w http.ResponseWriter, r *http.Request) {
		w.Header().Set("Content-Type", "application/json")
		w.Write([]byte(`{"message":"hello"}`))
	})
	// 链:超时 5s → 鉴权(secret) → api;"secret" 示例用,生产应从配置读取
	apiHandler := middleware.Timeout(5 * time.Second)(middleware.APIKeyAuth("secret")(api))
	mux.Handle("/api/", http.StripPrefix("/api", apiHandler))

	// 最外层:先打日志,再进 mux(再进超时、鉴权、业务)
	handler := middleware.Logging(mux)
	http.ListenAndServe(":8080", handler)
}

解读NewServeMux 替代默认 mux,便于挂子路由。/api/StripPrefix("/api", apiHandler),请求进来时会把路径前缀 /api 去掉再交给 apiHandler,所以 api 里注册的是 /hello。中间件链从里到外是:api → APIKeyAuth → Timeout;再整体交给 Logging(mux)。所以实际顺序是:Logging →(若 /api)Timeout → APIKeyAuth → 业务。

2. internal/middleware/logging.go

go 复制代码
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,返回新 Handler」。只有一层 return :直接返回 http.HandlerFunc(...)(一个 Handler),不像 Timeout/APIKeyAuth 那样「返回一个函数」。先记开始时间,调 next.ServeHTTP 把请求交给下一层,等下一层返回后再打日志(这样耗时包含业务执行时间)。

3. internal/middleware/auth.go

go 复制代码
func APIKeyAuth(apiKey string) func(http.Handler) http.Handler {
	return func(next http.Handler) http.Handler {
		return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
			key := r.Header.Get("X-API-Key")
			if key == "" || key != apiKey {
				w.Header().Set("Content-Type", "application/json")
				w.WriteHeader(http.StatusUnauthorized)
				w.Write([]byte(`{"error":"unauthorized"}`))
				return
			}
			next.ServeHTTP(w, r)
		})
	}
}

解读APIKeyAuth("secret") 返回「中间件函数」。在 Handler 里取 X-API-Key,不对就设 401、写 JSON、直接 return 不调 next ;对才 next.ServeHTTP(w, r)

4. internal/middleware/context_timeout.go

go 复制代码
func Timeout(d time.Duration) func(http.Handler) http.Handler {
	return func(next http.Handler) http.Handler {
		return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
			ctx, cancel := context.WithTimeout(r.Context(), d)
			defer cancel()
			next.ServeHTTP(w, r.WithContext(ctx))
		})
	}
}

解读两层 return :Timeout(d) 返回「一个函数」,这个函数接收 next 再返回 Handler;所以第一层 return 造「中间件函数」,第二层 return 造「包装后的 Handler」。逻辑上:用 r.Context() 作为父 context,WithTimeout 得到带超时的 ctxdefer cancel() 确保资源释放;关键next.ServeHTTP(w, r.WithContext(ctx)) 把带超时的 ctx 放进请求,业务里 r.Context() 就是它,超时后 ctx 被取消,下游可检测。


六、运行当天代码

在项目根目录执行:

bash 复制代码
go run ./day5/cmd/server
  • 健康检查:curl http://localhost:8080/health → 返回 ok;或浏览器打开 http://localhost:8080/health
  • 带鉴权:curl -H "X-API-Key: secret" http://localhost:8080/api/hello{"message":"hello"}
  • 不带 Key:curl http://localhost:8080/api/hello → 401;或浏览器直接打开 /api/hello 会得到 401(因无法带自定义 Header)

浏览器里带 Key 测试 :F12 → Console,先输入 allow pasting 回车(若提示不要粘贴代码),再执行:
fetch("http://localhost:8080/api/hello", { headers: { "X-API-Key": "secret" } }).then(r=>r.json()).then(console.log)

预期输出 {message: 'hello'}

可对比带 Key 与不带 Key 的响应,理解中间件如何拦截请求。

七、学习建议

  1. 理解链式顺序:请求依次经过「日志 → 超时 → 鉴权 → 业务」,对应代码里的包装顺序。
  2. 重点看 context :超时中间件如何创建带超时的 context、如何用 r.WithContext 传给下一层,面试常考。
  3. 改一改:改 API Key、超时时间,或加一个「打印 Header」的中间件,加深印象。
  4. 延伸阅读 :想了解签名、AK/SK、加解密等更多鉴权方式可参考 接口鉴权综述

八、小结

Day 5 的中间件和分层是写可维护 HTTP 服务的标配;Day 7 综合实战会复用「日志 + 可选鉴权 + 优雅关闭」。建议把中间件执行顺序和 context 传递搞清再进入 Day 6。

相关推荐
沐知全栈开发2 小时前
Python File 方法详解
开发语言
MX_93592 小时前
@Import整合第三方框架原理
java·开发语言·后端·spring
写代码的小球2 小时前
C++ 标准库 <numbers>
开发语言·c++·算法
拳里剑气2 小时前
C++:哈希
开发语言·数据结构·c++·算法·哈希算法·学习方法
坚持就完事了2 小时前
Java各种命名规则
java·开发语言
白露与泡影2 小时前
2026年Java面试题精选(涵盖所有Java核心面试知识点),立刻收藏
java·开发语言
瓦特what?2 小时前
冒 泡 排 序
开发语言·数据结构·c++
wjs20242 小时前
TypeScript 变量声明
开发语言
星火开发设计3 小时前
STL 容器:vector 动态数组的全面解析
java·开发语言·前端·c++·知识