不能用 c.Set() 存用户ID或租户ID,因为其数据仅限当前 Echo context 实例,不透传至 request.Context,导致 DB、日志等下游组件无法获取;必须用 context.WithValue() 写入 c.Request().Context() 并统一读取。

为什么不能用 c.Set() 存用户ID或租户ID
因为 c.Set() 写入的数据只在当前 Echo context 实例内有效,不会自动透传给后续中间件或下游调用。一旦你依赖第三方库(比如 sqlx.QueryRowContext())从 Context 拿用户信息做审计日志,它就完全拿不到——c.Set() 对 Context 是“不可见”的。
常见错误现象:
- 中间件 A 调用
c.Set("user_id", 123),中间件 B 里c.Get("user_id")返回nil - handler 里能取到值,但
db.QueryRowContext(c.Request().Context(), ...)中 DB 层查不到user_id
必须把值塞进 c.Request().Context() 才行
这是唯一能被所有下游组件(logger、DB、HTTP client、trace SDK)一致读取的载体。Echo 的请求上下文是链式传递的,只要你在中间件里用 context.WithValue() 包装并更新回请求,后续所有基于该 Context 的调用都能拿到。
实操要点:
- 键必须是自定义类型,比如
type ctxKey string,避免字符串键冲突 - 定义常量键:
const userIDKey ctxKey = "user_id" - 中间件中写入:
ctx := context.WithValue(c.Request().Context(), userIDKey, userID),再调用c.SetRequest(c.Request().WithContext(ctx)) - 下游 handler 或其他中间件统一用
c.Request().Context().Value(userIDKey)取值,不要混用c.Get()
别用全局变量或闭包捕获用户信息
看似省事,但会引发并发安全问题:多个请求共用同一变量,A 请求刚写入就被 B 请求覆盖,或者读到脏数据。Go 的 goroutine 是轻量级且高并发的,这种写法在压测或真实流量下必然出错。
Echo框架 5.1.0 版本源码包下载,适合关注 RealIP 行为变化、StartConfig.Listener、NewDefaultFS 和观测性中间件入口的开发团队。
正确做法只有两个:
- 所有用户态元数据都走
c.Request().Context(),靠键类型隔离 - 如果需要跨请求生命周期缓存(比如 token 解析结果),用线程安全的 map + sync.RWMutex,或更推荐的
sync.Map,但仅限只读场景
Logger 和 DB 怎么同时拿到 request_id 和 user_id
关键不是“怎么塞”,而是“塞到哪”——必须统一塞进 c.Request().Context(),并且确保所有组件都用这个 Context 发起调用。
例如:
- 日志中间件:从
c.Request().Context().Value(requestIDKey)取 ID,注入 log fields - DB 查询:必须用
db.QueryRowContext(c.Request().Context(), ...),而不是db.QueryRow(...) - HTTP outbound 调用:同样要用
http.DefaultClient.Do(req.WithContext(c.Request().Context()))
漏掉任意一环,数据链就断了。最常被忽略的是 DB 层没传 Context,导致审计字段为空——这不是框架问题,是调用姿势错了。

















