Skip to content

Latest commit

 

History

History
104 lines (71 loc) · 4.8 KB

9-1-Propagating-an-inappropriate-context.md

File metadata and controls

104 lines (71 loc) · 4.8 KB

9.1 传递不恰当的上下文

在 Go 中处理并发时,上下文无处不在,在许多情况下,可能建议传递它们。但是,有时上下文传递会导致细微的错误,从而阻止子功能正确执行。

让我们思考以下示例。我们公开了一个执行某些任务并返回响应的 HTTP 处理程序。然而,就在返回响应之前,我们还想将其发送到 Kafka 的 topic。由于我们不想增加 HTTP 消费者的响应时长,我们希望在新的 goroutine 中异步处理发布操作。假设我们有一个 publish 函数供我们使用,它接受一个上下文,这样如果上下文被取消,发布消息的操作就可以被中断,例如。

这将是一个可能的实现:

func handler(w http.ResponseWriter, r *http.Request) {
    response, err := doSomeTask(r.Context(), r)
    if err != nil {
        http.Error(w, err.Error(), http.StatusInternalServerError)
        return
    }

    go func() {
        err := publish(r.Context(), response)
        // Do something with err
    }()

    writeResponse(response)
}

首先,我们调用 doSomeTask 函数来获取 response 变量。它在 goroutine 调用 publish 和格式化 HTTP 响应中使用。此外,在调用 publish 时,我们会传递附加到 HTTP 请求的上下文。你能猜出这段代码有什么问题吗?

我们必须知道附加到 HTTP 请求的上下文可以在哪些的条件下取消:

  • 当客户端连接关闭时
  • 在 HTTP/2 请求下,当请求被取消时
  • 或者当响应被写回客户端时

在前两种情况下,我们可能会正确处理事情。例如,如果我们刚刚收到来自 doSomeTask 的响应,但客户端已关闭连接,则可以在上下文已取消的情况下调用 publish,这样消息就不会发布。但是最后一种情况呢?

当响应被写入客户端时,与请求关联的上下文将被取消。因此,我们面临一个竞争条件:

  • 如果写响应是在 Kafka 发布之后完成的,我们都会返回响应并成功发布消息
  • 但是,如果在 Kafka 发布之前或期间写回响应,则消息可能不会发布

在后一种情况下,调用 publish 会返回错误;只是因为我们很快返回了 HTTP 响应。

那么我们该如何解决这个问题呢?一种想法可能是不传递父上下文。相反,我们会使用空上下文调用 publish

err := publish(context.Background(), response)

这样写代码就可以正常工作了。无论写回 HTTP 响应需要多长时间,我们都可以调用 publish

然而,如果上下文包含一些有用的值怎么办?例如,如果上下文包含用于分布式跟踪的关联 ID,我们可以关联 HTTP 请求和 Kafka 发布。理想情况下,我们希望有一个新的上下文,与潜在的父取消分离,但仍然传达值。

标准包没有立即解决这个问题。因此,一个可能的解决方案是实现我们自己的 Go 上下文,类似于提供的上下文,只是它不携带取消信号。

context.Context 是一个包含 x 个方法的接口:

type Context interface {
    Deadline() (deadline time.Time, ok bool)
    Done() <-chan struct{}
    Err() error
    Value(key any) any
}

上下文的截止日期由 Deadline 方法管理,取消信号通过 DoneErr 方法管理。当截止日期已过或上下文已被取消时,Done 应该返回一个关闭的通道,而 Err 应该返回一个错误。最后,这些值是通过 Value 方法携带的。

让我们创建一个自定义上下文,它将取消信号从父上下文中分离出来:

type detach struct {
    ctx context.Context
}

func (d detach) Deadline() (time.Time, bool) {
    return time.Time{}, false
}

func (d detach) Done() <-chan struct{} {
    return nil
}

func (d detach) Err() error {
    return nil
}

func (d detach) Value(key any) any {
    return d.ctx.Value(key)
}

除了调用父上下文获取值的 Value 方法外,其他方法都返回一个默认值,因此上下文永远不会被视为过期或取消。

多亏了我们的自定义上下文,我们现在可以调用 publish 并且分离取消信号:

err := publish(detach{ctx: r.Context()}, response)

现在,传递给 publish 的上下文是一个永远不会过期也不会被取消的上下文,但它会携带父上下文的值。

总之,传递上下文应该谨慎进行。我们使用基于与 HTTP 请求关联的上下文处理异步操作的示例来说明本节。由于一旦我们返回响应上下文就会被取消,异步操作也可能会意外停止。让我们牢记传递给定上下文的影响,如果有必要,我们还要记住,始终可以为特定操作创建自定义上下文。

下一节将讨论一个常见的并发错误:启动一个 goroutine 而没有计划停止它。