第一章:Go中间件的核心设计哲学与演进脉络
Go中间件并非语言内置特性,而是社区在HTTP生态中逐步沉淀出的轻量、组合优先的设计范式。其本质是函数式链式调用的实践:每个中间件接收 http.Handler 并返回新的 http.Handler,形成可插拔、无状态、单职责的处理单元。
函数即中间件
Go标准库的 http.HandlerFunc 类型让中间件天然契合函数式思维。一个典型中间件签名如下:
// 中间件:记录请求耗时
func LoggingMiddleware(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))
})
}
该模式强调“包装而非侵入”——中间件不修改原始 handler 行为,仅在其前后注入横切逻辑,符合单一职责与开闭原则。
组合优于继承
中间件链通过嵌套或工具函数组装,例如使用 net/http 原生方式手动组合:
mux := http.NewServeMux()
mux.HandleFunc("/api/users", userHandler)
handler := LoggingMiddleware(RecoveryMiddleware(mux)) // 顺序决定执行栈
http.ListenAndServe(":8080", handler)
主流框架如 Gin、Echo 将此抽象为 Use() 方法,但底层仍是 func(http.Handler) http.Handler 的连续应用,体现“组合即配置”的哲学。
演进中的关键分水岭
| 阶段 | 特征 | 代表实践 |
|---|---|---|
| 原始封装 | 手动嵌套,易产生回调地狱 | 多层 http.HandlerFunc 嵌套 |
| 链式注册 | 框架提供 Use() / Add() 接口 |
Gin.Use(), Echo.Use() |
| 上下文增强 | 中间件间共享 context.Context |
r.Context().Value() 传递数据 |
| 异步与生命周期 | 支持 Before/After 钩子与异步日志 |
chi.Router.With() |
这种演进始终围绕一个内核:保持 handler 接口的纯粹性,让中间件成为可测试、可复用、可编排的函数构件。
第二章:中间件生命周期管理与上下文传递规范
2.1 Middleware函数签名设计:func(http.Handler) http.Handler vs func(http.Handler) http.Handler
看似重复的签名,实为Go HTTP中间件设计的唯一正解——它既是契约,也是约束。
为什么必须是 func(http.Handler) http.Handler?
- ✅ 符合装饰器模式:接收原始 handler,返回增强后的新 handler
- ✅ 保持接口正交:不侵入
http.Handler的ServeHTTP方法签名 - ❌
func(http.ResponseWriter, *http.Request)无法链式调用,丢失 handler 抽象
标准实现骨架
func LoggingMiddleware(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) // 调用下游 handler
log.Printf("END %s %s", r.Method, r.URL.Path)
})
}
逻辑分析:
next是上游传入的http.Handler(可能是另一个 middleware 或最终 handler);http.HandlerFunc(...)将闭包转为http.Handler实例,确保类型可组合。参数w和r原样透传,保障语义一致性。
中间件链式调用示意
graph TD
A[Request] --> B[LoggingMW]
B --> C[AuthMW]
C --> D[Router]
D --> E[Response]
2.2 Context传递的零拷贝实践:request-scoped value注入与类型安全提取
在高并发 HTTP 服务中,避免 request 生命周期内跨组件传递时的值拷贝至关重要。Go 的 context.Context 本身不支持泛型注入,但可通过 context.WithValue + 类型化 key 实现零拷贝的 request-scoped 值绑定。
类型安全的 key 定义
// 定义私有未导出类型,杜绝 key 冲突
type userIDKey struct{}
type traceIDKey struct{}
// 注入(零拷贝:仅传递指针/接口底层数据地址)
ctx = context.WithValue(ctx, userIDKey{}, uint64(12345))
ctx = context.WithValue(ctx, traceIDKey{}, "req-7f8a2c")
WithValue 不复制 value,仅将 (key, value) 对存入 context 链表节点;value 若为小结构体或指针,全程无内存拷贝。
类型安全提取
// 强制类型断言,失败返回零值 + false
if uid, ok := ctx.Value(userIDKey{}).(uint64); ok {
log.Printf("User ID: %d", uid) // 编译期无法校验,但运行时类型安全
}
| 提取方式 | 类型安全 | 零拷贝 | 推荐场景 |
|---|---|---|---|
ctx.Value(k).(T) |
✅(运行时) | ✅ | 简单、确定类型 |
自定义 FromContext 函数 |
✅(编译+运行) | ✅ | 生产级核心字段 |
graph TD
A[HTTP Request] --> B[Middleware A]
B --> C[Middleware B]
C --> D[Handler]
B -->|ctx.WithValue| C
C -->|ctx.WithValue| D
D -->|ctx.Value| E[Type-safe extract]
2.3 中间件链的构建与中断机制:next()调用时机与panic恢复策略
中间件链本质是函数式责任链,next() 是控制权移交的关键信号点。
next() 的三种典型调用时机
- 前置执行:日志、鉴权等无需等待下游结果的操作
- 后置包裹:
defer中执行响应头注入、耗时统计 - 条件跳过:如缓存命中时直接
return,不调用next()
panic 恢复的双层防护
func Recover() gin.HandlerFunc {
return func(c *gin.Context) {
defer func() {
if err := recover(); err != nil {
c.AbortWithStatusJSON(500, gin.H{"error": "internal error"})
}
}()
c.Next() // 此处调用可能触发panic
}
}
逻辑分析:
defer确保 panic 后仍能捕获;c.Next()触发后续中间件,若其内部 panic,由当前 defer 捕获。参数c是上下文载体,c.AbortWithStatusJSON终止链并写入错误响应。
| 场景 | 是否调用 next() | 链是否继续 |
|---|---|---|
| 正常流程 | ✅ | 是 |
| 提前 Abort() | ❌ | 否 |
| panic 被 Recover() | ✅(但已失效) | 否(已终止) |
graph TD
A[请求进入] --> B[中间件1:鉴权]
B --> C{鉴权通过?}
C -->|是| D[中间件2:日志]
C -->|否| E[返回401]
D --> F[c.Next()]
F --> G[业务Handler]
G --> H[panic?]
H -->|是| I[Recover中间件捕获]
H -->|否| J[正常返回]
2.4 并发安全的中间件状态管理:sync.Map vs context.WithValue vs 自定义RequestState结构体
在 HTTP 中间件中传递请求级状态时,需兼顾并发安全、生命周期可控与内存开销。
三种方案对比
| 方案 | 并发安全 | 生命周期 | 类型安全 | 适用场景 |
|---|---|---|---|---|
sync.Map |
✅ 原生支持 | 全局/长周期 | ❌ interface{} |
跨请求缓存(如 token 白名单) |
context.WithValue |
✅(读写串行化) | 请求生命周期 | ❌ interface{} |
简单透传(如用户ID、traceID) |
RequestState 结构体 |
✅(配合 context.WithValue(*RequestState)) |
请求生命周期 | ✅ 强类型字段 | 复杂中间件状态(如 auth、rate-limit、validation 结果) |
推荐实践:强类型 RequestState
type RequestState struct {
UserID int64 `json:"user_id"`
Scopes []string `json:"scopes"`
Deadline time.Time `json:"deadline"`
}
func WithRequestState(ctx context.Context, rs *RequestState) context.Context {
return context.WithValue(ctx, requestStateKey{}, rs)
}
此方式将
*RequestState作为值注入 context,避免类型断言错误;requestStateKey{}是未导出空结构体,确保 key 唯一且不可外部构造。结构体字段天然支持零值语义与 JSON 序列化,便于日志与调试。
数据同步机制
graph TD
A[HTTP Handler] --> B[Middleware A]
B --> C[Middleware B]
C --> D[Handler Logic]
B -.->|Write| E[(RequestState)]
C -.->|Read/Update| E
D -->|Read| E
2.5 性能敏感场景下的中间件裁剪:编译期条件注入与运行时动态注册对比实测
在高吞吐低延迟服务中,中间件链路开销需精确可控。两种主流裁剪策略存在本质差异:
编译期条件注入(零运行时分支)
#[cfg(feature = "redis_cache")]
pub struct RedisCacheMiddleware;
#[cfg(not(feature = "redis_cache"))]
pub struct RedisCacheMiddleware;
impl Middleware for RedisCacheMiddleware {
fn handle(&self, req: Request) -> Response {
#[cfg(feature = "redis_cache")]
{ /* 实际缓存逻辑 */ }
#[cfg(not(feature = "redis_cache"))]
{ req } // 编译期直接移除调用栈
}
}
✅ 优势:无虚函数调用、无 trait object 动态分发、无条件判断;#[cfg] 在编译期彻底剥离未启用模块,二进制体积与指令路径均最小化。
运行时动态注册(灵活性代价)
| 指标 | 编译期注入 | 运行时注册 |
|---|---|---|
| 启动延迟 | 0μs(无注册逻辑) | ~120μs(HashMap 插入+反射解析) |
| P99 延迟 | 8.3μs | 14.7μs |
| 内存占用 | 静态只读段 | +1.2MB(注册表+元数据) |
graph TD
A[请求进入] --> B{是否启用Redis中间件?}
B -->|编译期已剔除| C[直通Handler]
B -->|运行时查表| D[调用动态分发器]
D --> E[实际中间件逻辑]
实测表明:在 50K QPS 的风控网关中,编译期裁剪降低 CPU cache miss 率 37%,而运行时注册引入的间接跳转使 L1d 缓存未命中上升 2.1×。
第三章:可观测性中间件工程化落地
3.1 分布式追踪集成:OpenTelemetry SDK嵌入与Span生命周期对齐
OpenTelemetry SDK 不是被动埋点工具,而是需与应用执行流深度耦合的可观测性运行时。关键在于 Span 的创建、激活、结束与上下文传播必须严格对齐业务逻辑生命周期。
Span 生命周期三阶段
- 启动:
Tracer.startSpan()触发,绑定当前Context(含 TraceID/ParentSpanID) - 活跃:通过
Scope scope = tracer.withSpan(span).makeCurrent()激活上下文,确保后续操作自动继承 Span - 终止:显式调用
span.end()—— 延迟结束将导致指标失真与内存泄漏
上下文传播机制
// 在 HTTP 入口处提取并激活远程上下文
Context extracted = W3CPropagator.getInstance().extract(Context.current(), requestHeaders, getter);
Scope scope = extracted.makeCurrent(); // 激活后,所有子 Span 自动关联
此代码在请求入口注入远程 Trace 上下文;
getter是自定义 Header 读取器,确保traceparent字段被正确解析;makeCurrent()将 Span 绑定至当前线程本地Context,为后续异步调用提供可继承性。
SDK 初始化要点
| 配置项 | 推荐值 | 说明 |
|---|---|---|
otel.traces.exporter |
otlp |
统一使用 OTLP 协议对接后端 |
otel.sdk.disabled |
false |
确保 SDK 实际启用 |
otel.context.propagation.type |
tracecontext,baggage |
同时支持追踪与业务元数据透传 |
graph TD
A[HTTP 请求进入] --> B[Extract traceparent]
B --> C[Start Root Span]
C --> D[Activate Scope]
D --> E[业务逻辑执行]
E --> F[End Span]
F --> G[异步上报至 Collector]
3.2 结构化日志中间件:字段化日志生成、采样控制与日志上下文继承
结构化日志中间件将传统文本日志升级为机器可解析的键值对序列,核心能力涵盖字段化生成、动态采样与跨协程/HTTP调用链的上下文继承。
字段化日志生成示例
log.Info("user_login_success",
zap.String("user_id", "u_8a9b"),
zap.Int64("duration_ms", 142),
zap.String("client_ip", "192.168.3.5"),
zap.String("auth_method", "oauth2"))
该调用生成 JSON 日志:
{"level":"info","msg":"user_login_success","user_id":"u_8a9b","duration_ms":142,"client_ip":"192.168.3.5","auth_method":"oauth2"}。zap.String等函数确保类型安全与零分配序列化。
采样策略对照表
| 策略 | 触发条件 | 适用场景 |
|---|---|---|
| 固定比率 | sampled := rand.Float64() < 0.01 |
全量日志降噪 |
| 错误优先 | if level == ERROR { emit() } |
故障快速定位 |
| 请求ID白名单 | if traceID == "t-abc123" { emitAll() } |
调试特定链路 |
上下文继承机制
graph TD
A[HTTP Handler] -->|WithContext| B[DB Query]
B -->|Inherit| C[Cache Lookup]
C -->|Propagate| D[Async Notification]
所有子操作自动继承 request_id、trace_id、user_id 等上下文字段,无需手动透传。
3.3 指标埋点标准化:Prometheus Counter/Gauge/Histogram在中间件层的语义化暴露
中间件指标需严格遵循语义契约,避免“指标滥用”。例如,请求计数必须用 Counter(单调递增),活跃连接数必须用 Gauge(可增可减),而响应延迟必须用 Histogram(分桶统计)。
语义误用反例与修正
- ❌ 将并发请求数暴露为
Counter→ 无法反映瞬时状态 - ✅ 改为
Gauge并配合promhttp.NewGaugeVec
典型中间件指标定义(Go + client_golang)
// 中间件层语义化指标注册示例
var (
httpRequestsTotal = prometheus.NewCounterVec(
prometheus.CounterOpts{
Namespace: "middleware",
Subsystem: "http",
Name: "requests_total", // 语义:累计请求数(不可回退)
Help: "Total number of HTTP requests.",
},
[]string{"method", "status_code", "route"},
)
activeConnections = prometheus.NewGauge(
prometheus.GaugeOpts{
Namespace: "middleware",
Subsystem: "tcp",
Name: "active_connections", // 语义:当前活跃连接(实时波动)
Help: "Current number of active TCP connections.",
},
)
requestDuration = prometheus.NewHistogram(
prometheus.HistogramOpts{
Namespace: "middleware",
Subsystem: "http",
Name: "request_duration_seconds", // 语义:延迟分布(含le标签)
Help: "HTTP request duration in seconds.",
Buckets: prometheus.ExponentialBuckets(0.001, 2, 12), // 1ms~2s共12桶
},
)
)
逻辑分析:
CounterVec支持多维标签聚合,适用于按method/status_code/route下钻分析;Gauge直接调用Set()更新当前值;Histogram自动注入le标签并计算_sum/_count/_bucket三元组,支撑rate()与histogram_quantile()查询。
| 指标类型 | 适用场景 | 不可重置性 | Prometheus 聚合友好性 |
|---|---|---|---|
| Counter | 累计事件(如请求、错误) | ✅ 强制单调增 | ✅ rate() 安全 |
| Gauge | 瞬时状态(如内存、连接) | ❌ 可任意设 | ⚠️ avg_over_time() 合理 |
| Histogram | 分布类(如延迟、大小) | ✅ 桶计数只增 | ✅ 支持分位数计算 |
graph TD
A[中间件启动] --> B[注册语义化指标]
B --> C{请求抵达}
C --> D[Counter.Inc method=GET status_code=200]
C --> E[Gauge.Set current_conn=42]
C --> F[Histogram.Observe 0.045]
第四章:高保障中间件模式实战(腾讯/字节/滴滴联合验证)
4.1 熔断降级中间件:基于gobreaker的自适应阈值配置与业务错误码白名单机制
传统熔断器依赖固定阈值(如错误率 >50%),难以适配动态流量与业务语义。gobreaker 提供扩展接口,支持运行时注入自定义判定逻辑。
白名单驱动的错误分类
var businessErrorWhitelist = map[int]bool{
400: true, // 参数校验失败,非系统异常
401: true, // 认证失效,可重试
404: true, // 资源不存在,业务正常态
}
该映射将 HTTP 状态码归类为“非故障信号”,避免误熔断;仅 5xx 及未登记错误参与熔断统计。
自适应阈值计算逻辑
func adaptiveThreshold() float64 {
rps := getRecentRPS() // 过去30s平均QPS
base := 0.2 // 基线错误率
delta := math.Max(0, 0.3-rps/100) // 流量越低,容忍度越高
return base + delta
}
阈值随实时 RPS 动态衰减,在低流量期提升容错性,防止“冷启误熔”。
| 错误类型 | 是否计入熔断 | 说明 |
|---|---|---|
500, 503 |
✅ | 后端服务不可用 |
400, 404 |
❌ | 业务合法响应 |
| 自定义 bizErr | ❌ | 实现 IsBusinessError() 接口 |
graph TD
A[请求入口] --> B{是否白名单错误码?}
B -->|是| C[跳过熔断计数]
B -->|否| D[更新错误计数 & 检查阈值]
D --> E{错误率 > 自适应阈值?}
E -->|是| F[切换至 OPEN 状态]
4.2 认证鉴权中间件:JWT+RBAC+ABAC混合策略解析与细粒度资源权限缓存
传统单点授权模型难以应对微服务中动态、多维的访问控制需求。本方案融合 JWT 的无状态认证优势、RBAC 的角色层级管理能力,以及 ABAC 的属性化动态决策机制,构建三级权限校验流水线。
混合策略执行时序
// 权限校验中间件核心逻辑(Express.js)
app.use(async (req, res, next) => {
const token = req.headers.authorization?.split(' ')[1];
const payload = verifyJWT(token); // 验证签名并解码,含 userId、roles、attrs(如 deptId、isVIP)
const cachedPerms = await redis.get(`perms:${payload.userId}`); // 缓存键含用户ID+租户ID+环境标识
if (cachedPerms) return checkABAC(cachedPerms, req); // 基于资源属性(如 resourceType=order, action=delete, context=prod)实时评估
next();
});
该中间件首先完成 JWT 解析与基础身份可信验证;verifyJWT 返回结构化声明,其中 attrs 字段为 ABAC 提供运行时上下文;redis.get 使用复合 key 实现跨租户隔离的细粒度缓存,避免重复加载 RBAC 角色-权限映射。
权限缓存维度设计
| 维度 | 示例值 | 更新触发条件 |
|---|---|---|
| 用户ID | usr_7a2f |
密码重置、角色变更 |
| 租户ID | tnt_3e8c |
租户配置更新 |
| 环境标签 | env:staging |
发布流程切换 |
决策流程
graph TD
A[收到请求] --> B{JWT有效?}
B -->|否| C[401 Unauthorized]
B -->|是| D[读取缓存权限集]
D --> E{缓存命中?}
E -->|否| F[查DB+RBAC+ABAC联合计算]
E -->|是| G[ABAC实时属性匹配]
G --> H{允许访问?}
H -->|否| I[403 Forbidden]
H -->|是| J[放行]
4.3 流量治理中间件:令牌桶限流+优先级队列+请求透传标签(trace_id、env、zone)
在高并发网关层,单一限流策略易导致关键链路被淹没。我们采用分层流量治理模型:前置令牌桶实现粗粒度速率控制,中层优先级队列保障核心业务SLA,后端通过透传标签实现全链路可溯。
核心组件协同逻辑
// 令牌桶 + 优先级调度示例(伪代码)
RateLimiter limiter = RateLimiter.create(100.0); // QPS=100
PriorityQueue<Request> queue = new PriorityQueue<>(
Comparator.comparing(r -> r.priority).reversed()
);
queue.offer(new Request(traceId, "prod", "shanghai", PRIORITY_HIGH));
RateLimiter.create(100.0):Guava限流器,平滑突发流量,支持预热;PRIORITY_HIGH:基于env=prod与zone=shanghai动态升权,避免跨域降级;traceId全程透传,用于熔断/告警上下文关联。
请求标签透传规范
| 字段 | 类型 | 必填 | 示例值 | 用途 |
|---|---|---|---|---|
trace_id |
String | 是 | trc-8a9b2c1d |
全链路追踪ID |
env |
String | 是 | prod / staging |
环境隔离与灰度路由 |
zone |
String | 否 | beijing |
地域亲和性调度 |
graph TD
A[Client] -->|注入trace_id/env/zone| B(Gateway)
B --> C{令牌桶限流}
C -->|通过| D[优先级队列]
D -->|高优先出| E[Service]
C -->|拒绝| F[返回429]
4.4 数据脱敏中间件:响应体AST遍历脱敏与敏感字段路径声明式配置(支持JSONPath & Protobuf反射)
核心设计思想
将脱敏逻辑从业务代码解耦,通过响应体抽象语法树(AST)遍历实现零侵入脱敏,结合声明式敏感路径配置统一管控。
脱敏执行流程
graph TD
A[HTTP响应体] --> B{Content-Type}
B -->|application/json| C[Jackson JsonNode AST]
B -->|application/x-protobuf| D[Protobuf DynamicMessage]
C --> E[JSONPath路径匹配]
D --> F[FieldDescriptor反射遍历]
E & F --> G[按规则替换值]
配置示例(YAML)
sensitivePaths:
- path: "$.user.id" # JSONPath
rule: "mask:4"
- path: "UserInfo.email" # Protobuf全限定名
rule: "hash:sha256"
支持的脱敏规则类型
| 规则 | 说明 | 示例 |
|---|---|---|
mask:n |
保留前n位,其余掩码 | "138****1234" |
hash:algo |
哈希脱敏(SHA256/MD5) | "e3b0c442..." |
redact |
替换为[REDACTED] |
"[REDACTED]" |
第五章:Go中间件生态演进趋势与架构决策指南
中间件分层治理实践:从单体拦截到领域感知
某电商中台在2023年重构网关层时,将传统统一中间件链(日志→鉴权→限流→监控)拆分为三层:基础设施层(如 grpc-go 的 UnaryInterceptor)、业务域层(如 order-middleware 包内封装的库存预占校验、优惠券幂等钩子)、场景适配层(针对小程序/H5/APP端分别注入差异化埋点字段)。该结构使中间件复用率提升67%,并通过 go:embed 内嵌 YAML 配置实现运行时动态加载:
type MiddlewareConfig struct {
Enabled bool `yaml:"enabled"`
Order int `yaml:"order"`
Params map[string]any `yaml:"params"`
}
生态工具链成熟度对比分析
| 工具库 | 插件热加载 | OpenTelemetry 原生支持 | 配置驱动能力 | 社区活跃度(GitHub Stars) |
|---|---|---|---|---|
go-chi/chi |
❌ | ✅(v5.1+) | ✅(Router.Group) | 24.8k |
gin-gonic/gin |
⚠️(需重载实例) | ✅(官方 middleware) | ✅(Use()链式) | 64.2k |
labstack/echo |
✅(WithContext) | ✅(v4.10+) | ✅(MiddlewareFunc) | 28.5k |
WASM 边缘中间件的生产验证
字节跳动在 CDN 边缘节点部署基于 wasmer-go 的轻量中间件,将用户设备指纹解析逻辑编译为 WASM 模块。Go 主程序通过 wasmer.NewEngine() 加载模块,在 3ms 内完成 UA 解析与地域路由决策,QPS 提升至 12.4k(原 Go 实现为 8.9k)。关键代码片段:
engine := wasmer.NewEngine()
store := wasmer.NewStore(engine)
module, _ := wasmer.NewModule(store, wasmBytes)
instance, _ := wasmer.NewInstance(module, store)
result, _ := instance.Exports["parse_ua"].Call(ctx, uint64(unsafe.Pointer(&uaBuf[0])))
架构决策树:何时放弃中间件链?
当出现以下信号时需重构中间件模式:
- 单个请求经过 >7 层中间件且存在跨层依赖(如第5层需读取第2层生成的上下文字段)
- 中间件执行耗时方差 >200ms(Prometheus 指标
middleware_duration_seconds{quantile="0.99"}) - 灰度发布需对某中间件做 A/B 测试但无法独立灰度(因硬编码在
router.Use()中)
graph TD
A[新功能上线] --> B{是否需要独立生命周期管理?}
B -->|是| C[采用 Service Mesh Sidecar]
B -->|否| D{是否需跨服务复用?}
D -->|是| E[提取为独立 gRPC 微服务]
D -->|否| F[保留中间件形式]
C --> G[Envoy Filter + Go WASM]
E --> H[Protobuf 接口定义 + gRPC Health Check]
零信任中间件的落地挑战
某金融客户在接入 open-policy-agent/opa-go 时发现:OPA Rego 策略加载延迟导致首请求超时。解决方案是预编译策略为 .rego 字节码并缓存至 sync.Map,同时将策略评估下沉至 http.ResponseWriter 的 WriteHeader 阶段前执行,避免阻塞主流程。
中间件可观测性增强方案
使用 otelhttp.WithFilter 过滤健康检查路径后,为每个中间件注入唯一 trace ID 标签:
func TraceMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
span := trace.SpanFromContext(ctx)
span.SetAttributes(attribute.String("middleware.name", "auth"))
next.ServeHTTP(w, r)
})
} 