golang 似乎为了保证线程安全,context 不允许修改,只能继承,但这样带来的问题就是上文环境无法获取在下文中更新的 context 。
func left(ctx context.Context) {
right(ctx)
value := GetContextValue(ctx, "key")
fmt.Println(value)
}
func right(ctx context.Context) {
ctx := context.WithValue(ctx, "key", "value")
}
因为 right 中 context 并没有改变旧的 ctx ,因此 left 中无法获取到 key
的值。
我的想法是 ctx 里面塞一个指针,不知道这样是否合理。
// 类似这样,可能不是很准确
func right(ctx context.Context) {
sctx := ctx.Value("context").(*SyncContext)
sctx.Set(...)
}
func left(ctx context.Context) {
sctx := ctx.Value("context").(*SyncContext)
right(ctx)
fmt.Println(sctx.Get(...))
}
type SyncContext struct {
values sync.Map
}
func NewSyncContext() *SyncContext { ... }
func (c *SyncContext) Get(key string) any { ... }
func (c *SyncContext) Set(key string, value any) { ... }
func main() {
ctx := context.WithValue(context.Background(), "context", NewSyncContext())
left(ctx)
}
但感觉这种姿势怪怪的。有没有其他的想法?
大概描述一下我的具体场景,http middleware 使用链式调用,第一个中间件是日志中间件,会在所有 next 调用结束后输出日志,请求、响应这些目前都有办法获取了,就是 next 中间件往 req.Context()
写的数据读不到(因为 req.WithContext
也会创建新的 request ,而不是修改 request 的 ctx ,目前看到的代码也没有提供修改 request context 的途径)。
主要是 next 中间件会进行一些身份认证,会把用户信息写进 context ,需要日志最后打出这些用户信息 ( PS:因为这些日志是需要以特定格式输出用于审计的,所以各个中间件自行输出可能会比较难受,主要是想各司其职,不要把心智负担下放到下游中间件)。
1
monsterxx03 285 天前 1
*req = *req.WithContext(...)
|
2
singer 285 天前 1
不怪,这么处理合理。参考 gin 框架,https://github.com/gin-gonic/gin/blob/master/context.go#L69 。上下文中传递轻量数据,一个 map 足够了,你认为会有并发,那就 sync.map 。
|
3
kuanat 285 天前 1
Go 的 context 是 1.7 版本引入给 net/http 服务的,用来解决信号和取消问题,传 value 只是顺带的,同时特别强调了线程安全的问题。名字用了 context 但是语义上确实只有上文。所以当你真正需要上下文的时候 context 包是不够的。
一般中间件解决这个问题的思路是自定义 context ,其实我不太喜欢 gin 的方式,我个人的偏好是类似 ``` type MyContext struct { ctx context.Context // custom field key string } ``` 这样的方式。然后实现 Context 的接口方法,写几个 wrapper 就可以完成对 context.Context 的兼容,不影响原本 net/http 的信号取消机制。 剩下的就是语法层面的封装了,需要实现一组方法,比如从 context.Context 衍生出子 MyContext: ``` func DeriveMyContext(ctx context.Context) *MyContext { myCtx, _ := ctx.Value(MyCtxKey).(*MyContext) return myCtx } ``` 此处用接口断言是根据 context 的设计,value 通过自定义类型模拟命名空间,防止 key 冲突。 结合起来就是 `context.WithValue(context.Background(), key, value)` 中的 kv 对,实际上就是通过 context.Value 传递了一个特定的 key ,这个 key 等价于指向 MyContext 的指针,和你的思路是一致的。 这样中间件所有涉及的 context 都通过一个 MyContext 的结构共享上下文,如果涉及到多线程可以加 Mutex 锁。 反正 Go 在传递 context 这件事上已经一条道走到黑了,比如 1.21 标准化的 slog 日志库也可以接受 context ,稍微封装下也可以直接用。 |
4
lvlongxiang199 285 天前
建议还是把鉴权放到 log 之前. 向 ctx 里头塞指针, 万一有地方把指针里的值改了, 很难 debug, 不如让它不可变
|
5
mainjzb 285 天前
gin 的 ctx 有 set 方法, 内部维护了一个 map
|
6
mainjzb 285 天前
gin 是这么用的。内部维护一个 map
// 中间件 c.Set("user_id", s.UserID) c.Set("session_id", s.ID) c.Set("token", s.AccessToken) 后面的 handler 直接 c.Get("user_id") 获取即可。 |
7
SSang OP @lvlongxiang199 链式调用,把 log 放后面无法保证一定被调用,否则要单独抽一个逻辑,但其实不只是 log 中间件需要获取响应,所以会变得很不通用
|
9
DefoliationM 285 天前 via Android
最开始塞个 map 进去,之后直接往 map 里存
|
10
rrfeng 285 天前
这个不是 context 包要解决的问题
你需要的是 http 的 RequestContext ,比如楼上说的 gin 的,可以直接 Set/Get 任意值。 |
11
wqtacc 285 天前
像下面这样使用
```go func left(ctx context.Context) { ctx = right(ctx) value := ctx.Value("key") fmt.Println(value) } func right(ctx context.Context) context.Context { return context.WithValue(ctx, "key", "value") } ``` |
12
iceheart 285 天前 via Android
Context 就是一棵树,想咋玩就咋玩喽
|
13
lvlongxiang199 284 天前
@SSang 似乎可以这样,
middlewareA:|________________________将 user_id 等信息放入 ctx______________________| middlewareB: |____________________________log__________________________| middlewareC: |_________________如果没有 user_id 报错_____| |
14
lvlongxiang199 284 天前
@SSang 似乎可以这样,
``` middlewareA:|________________________将 user_id 等信息放入 ctx______________________| middlewareB: |____________________________log__________________________| middlewareC: |_________________如果没有 user_id 报错_____| ``` |
15
flighter 284 天前
去实现 自己的 MyContext 去做这个事情
|