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

概述

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


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

更多关于Golang Go语言中的Context为什么只有上文没有下文?一般如何传递下文?的实战教程也可以访问 https://www.itying.com/category-94-b0.html

16 回复

*req = *req.WithContext(…)

更多关于Golang Go语言中的Context为什么只有上文没有下文?一般如何传递下文?的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html


不怪,这么处理合理。参考 gin 框架,https://github.com/gin-gonic/gin/blob/master/context.go#L69 。上下文中传递轻量数据,一个 map 足够了,你认为会有并发,那就 sync.map 。

Go 的 context 是 1.7 版本引入给 net/http 服务的,用来解决信号和取消问题,传 value 只是顺带的,同时特别强调了线程安全的问题。名字用了 context 但是语义上确实只有上文。所以当你真正需要上下文的时候 context 包是不够的。

一般中间件解决这个问题的思路是自定义 context ,其实我不太喜欢 gin 的方式,我个人的偏好是类似

<br>type MyContext struct {<br> ctx context.Context<br> // custom field<br> key string<br>}<br>
这样的方式。然后实现 Context 的接口方法,写几个 wrapper 就可以完成对 context.Context 的兼容,不影响原本 net/http 的信号取消机制。

剩下的就是语法层面的封装了,需要实现一组方法,比如从 context.Context 衍生出子 MyContext:
<br>func DeriveMyContext(ctx context.Context) *MyContext {<br> myCtx, _ := ctx.Value(MyCtxKey).(*MyContext)<br> return myCtx<br>}<br>
此处用接口断言是根据 context 的设计,value 通过自定义类型模拟命名空间,防止 key 冲突。

结合起来就是 context.WithValue(context.Background(), key, value) 中的 kv 对,实际上就是通过 context.Value 传递了一个特定的 key ,这个 key 等价于指向 MyContext 的指针,和你的思路是一致的。

这样中间件所有涉及的 context 都通过一个 MyContext 的结构共享上下文,如果涉及到多线程可以加 Mutex 锁。

反正 Go 在传递 context 这件事上已经一条道走到黑了,比如 1.21 标准化的 slog 日志库也可以接受 context ,稍微封装下也可以直接用。

建议还是把鉴权放到 log 之前. 向 ctx 里头塞指针, 万一有地方把指针里的值改了, 很难 debug, 不如让它不可变

gin 的 ctx 有 set 方法, 内部维护了一个 map

gin 是这么用的。内部维护一个 map

// 中间件
c.Set(“user_id”, s.UserID)
c.Set(“session_id”, s.ID)
c.Set(“token”, s.AccessToken)

后面的 handler 直接 c.Get(“user_id”) 获取即可。

链式调用,把 log 放后面无法保证一定被调用,否则要单独抽一个逻辑,但其实不只是 log 中间件需要获取响应,所以会变得很不通用

感谢,你这个写法比我的好。我也是不太喜欢 gin 的方式,我也是希望尽可能兼容官方接口。

最开始塞个 map 进去,之后直接往 map 里存

这个不是 context 包要解决的问题
你需要的是 http 的 RequestContext ,比如楼上说的 gin 的,可以直接 Set/Get 任意值。

像下面这样使用

go<br>func left(ctx context.Context) {<br> ctx = right(ctx)<br> value := ctx.Value("key")<br> fmt.Println(value)<br>}<br><br>func right(ctx context.Context) context.Context {<br> return context.WithValue(ctx, "key", "value")<br>}<br>

Context 就是一棵树,想咋玩就咋玩喽

似乎可以这样,

middlewareA:|__将 user_id 等信息放入 ctx|
middlewareB: |__log|
middlewareC: |____________如果没有 user_id 报错|

似乎可以这样,
<br>middlewareA:|________________________将 user_id 等信息放入 ctx______________________|<br>middlewareB: |____________________________log__________________________|<br>middlewareC: |_________________如果没有 user_id 报错_____|<br>

去实现 自己的 MyContext 去做这个事情

在Golang中,Context的设计初衷是为了在API边界之间和请求范围内传递截止日期、取消信号以及其他请求范围的值,而不是传统意义上的“上下文”概念,它更侧重于传递“控制”信息而非完整的上下文数据。因此,它更多地被理解为“控制上下文”或“请求上下文”,而不是包含完整“上文”和“下文”的广义上下文。

关于为什么Golang的Context只有“上文”(即传递的信息是从创建点向使用点单向流动的),这主要是因为它的设计目标是轻量级、高效且易于管理。它避免了在请求处理过程中传递大量数据,从而减少了内存占用和复杂性。

一般传递“下文”(如果这里指的是需要在请求处理过程中传递的额外信息)的方式,并不是通过Context本身来实现的。Context通常用于传递请求范围的元数据,如超时、取消信号等。如果需要传递其他业务相关的数据,通常会使用其他机制,如结构体参数、全局状态管理(虽然不推荐)或通过中间件/拦截器模式在请求处理链中传递。

在实际应用中,开发者可以通过Context的WithValue方法添加键值对来传递少量、请求范围的数据,但应谨慎使用以避免滥用Context作为通用数据存储。对于复杂的业务逻辑,建议采用更结构化的数据传递方式。

回到顶部