V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
The Go Programming Language
http://golang.org/
Go Playground
Go Projects
Revel Web Framework
SSang
V2EX  ›  Go 编程语言

Golang 中的 Context 为什么只有上文没有下文?一般如何传递下文?

  •  
  •   SSang · 285 天前 · 3075 次点击
    这是一个创建于 285 天前的主题,其中的信息可能已经有所发展或是发生改变。

    概述

    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:因为这些日志是需要以特定格式输出用于审计的,所以各个中间件自行输出可能会比较难受,主要是想各司其职,不要把心智负担下放到下游中间件)。

    15 条回复    2024-01-30 19:54:42 +08:00
    monsterxx03
        1
    monsterxx03  
       285 天前   ❤️ 1
    *req = *req.WithContext(...)
    singer
        2
    singer  
       285 天前   ❤️ 1
    不怪,这么处理合理。参考 gin 框架,https://github.com/gin-gonic/gin/blob/master/context.go#L69 。上下文中传递轻量数据,一个 map 足够了,你认为会有并发,那就 sync.map 。
    kuanat
        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 ,稍微封装下也可以直接用。
    lvlongxiang199
        4
    lvlongxiang199  
       285 天前
    建议还是把鉴权放到 log 之前. 向 ctx 里头塞指针, 万一有地方把指针里的值改了, 很难 debug, 不如让它不可变
    mainjzb
        5
    mainjzb  
       285 天前
    gin 的 ctx 有 set 方法, 内部维护了一个 map
    mainjzb
        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") 获取即可。
    SSang
        7
    SSang  
    OP
       285 天前
    @lvlongxiang199 链式调用,把 log 放后面无法保证一定被调用,否则要单独抽一个逻辑,但其实不只是 log 中间件需要获取响应,所以会变得很不通用
    SSang
        8
    SSang  
    OP
       285 天前
    @kuanat 感谢,你这个写法比我的好。我也是不太喜欢 gin 的方式,我也是希望尽可能兼容官方接口。
    DefoliationM
        9
    DefoliationM  
       285 天前 via Android
    最开始塞个 map 进去,之后直接往 map 里存
    rrfeng
        10
    rrfeng  
       285 天前
    这个不是 context 包要解决的问题
    你需要的是 http 的 RequestContext ,比如楼上说的 gin 的,可以直接 Set/Get 任意值。
    wqtacc
        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")
    }
    ```
    iceheart
        12
    iceheart  
       285 天前 via Android
    Context 就是一棵树,想咋玩就咋玩喽
    lvlongxiang199
        13
    lvlongxiang199  
       284 天前
    @SSang 似乎可以这样,

    middlewareA:|________________________将 user_id 等信息放入 ctx______________________|
    middlewareB: |____________________________log__________________________|
    middlewareC: |_________________如果没有 user_id 报错_____|
    lvlongxiang199
        14
    lvlongxiang199  
       284 天前
    @SSang 似乎可以这样,
    ```
    middlewareA:|________________________将 user_id 等信息放入 ctx______________________|
    middlewareB: |____________________________log__________________________|
    middlewareC: |_________________如果没有 user_id 报错_____|
    ```
    flighter
        15
    flighter  
       284 天前
    去实现 自己的 MyContext 去做这个事情
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   1649 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 24ms · UTC 16:51 · PVG 00:51 · LAX 08:51 · JFK 11:51
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.