译文 Contexts and structs

Cluas · 2021年04月13日 · 69 次阅读

Contexts and structs

Jean de Klerk, Matt T. Proud 24 February 2021

介绍

在很多的 Go APIs 中,尤其是最新的一些,函数和方法的第一个参数常常是context.ContextContext提供了一种跨 API 边界以及在Goroutine之间传输截止时间,调用者取消以及其他请求范围的值的方法。当一个库与远程服务器(例如数据库,API 等)直接或者通过传递交互时,通常会使用它。

context 的文档指出:

Contexts不应该存储在struct内部,而应传递给需要它的每个函数。

本文在次建议的基础上扩展了原因和示例,描述了为什么传递Context而不是将其存储在另一种类型中为什么很重要。还强调了一种特殊的情况,即以struct存储Context以及如何安全地存储 Context 是有意义的。

首选将 Context 为参数传递

要了解不要讲 Context 存储在 struct 的建议,让我们先考虑首选的 “Context 作为参数” 方式:

type Worker struct { /* … */ }

type Work struct { /* … */ }

func New() *Worker {
  return &Worker{}
}

func (w *Worker) Fetch(ctx context.Context) (*Work, error) {
  _ = ctx // A per-call ctx is used for cancellation, deadlines, and metadata.
}

func (w *Worker) Process(ctx context.Context, work *Work) error {
  _ = ctx // A per-call ctx is used for cancellation, deadlines, and metadata.
}

在此,(*Worker).Fetch(*Worker).Process方法都直接接受Context。通过这种按参数传递设计,用户可以设置每次呼叫的截止时间,取消和元数据。而且,很清楚如何使用context.Context传递给每个方法:不会期望传递给一个方法的context.Context将被其他任何方法使用。这是因为上下文的范围仅限于所需的操作,这大大提高了context此程序包的实用性和清晰度。

将上下文存储在结构中会导致混乱

让我们Worker使用不咋好的context-in-struct方法再次检查上面的示例。它的问题是,当您将 Context 存储在 struct 中时,调用者的生命周期就变得晦涩难懂,或者更糟的是,这两种范围以不可预测的方式混合在一起:

type Worker struct {
  ctx context.Context
}

func New(ctx context.Context) *Worker {
  return &Worker{ctx: ctx}
}

func (w *Worker) Fetch() (*Work, error) {
  _ = w.ctx // A shared w.ctx is used for cancellation, deadlines, and metadata.
}

func (w *Worker) Process(work *Work) error {
  _ = w.ctx // A shared w.ctx is used for cancellation, deadlines, and metadata.
}

(*Worker).Fetch(*Worker).Process方法都使用存储在WorkerContext。这样阻碍了FetchProcess(本身可能具有不同的Context)的调用方指定截止日期,请求取消以及按每次调用附加元数据。例如:用户无法仅提供的截止日期(*Worker).Fetch,或仅取消(*Worker).Process调用。调用者的生存期与共享Context混合在一起,并且Context的范围仅限于Worker创建的生命周期。

与通过参数传递方法相比,该 API 对用户也更加令人困惑。用户可能会问自己:

  • 由于New采用context.Context,构造函数是在做需要取消或截止日期的工作吗?
  • 是否通过New存储的context.Context同时被用于(*Worker).Fetch(*Worker).Process?两者都不?一个是而另一个不是?

该 API 需要大量文档,以明确告诉用户确切的 context.Context 用途。用户可能还必须阅读代码,而不是能够依赖于 API 传达的结构。

最后还有一点,这是一个相当危险的设计在生产级服务器上,该服务器的请求每个都不具有 Context,因此不能充分兑现取消要求。如果无法设置每次调用的的 deadlines,您的程序可能会积压并耗尽其资源(如内存 OOM)!

规则的例外:保留向后兼容性

当 Go 1.7(引入了context.Context)发布时,大量的 API 必须以向后兼容的方式添加Context支持。例如,net/httpClient的方法,如GetDo,是Context的优良候选。使用这些方法发送的每个外部请求都将受益于附带的截止日期,取消和元数据支持通过携带context.Context

有两种方法可以context.Context以向后兼容的方式添加对它的支持:稍后将看到的讲Context保存在struct中,以及复制function,其中重复项接受 context.Context 作为参数并以 Context 作为函数名后缀。采用重复 function 应优先于context-in-struct,并且在保持模块兼容中将进一步讨论。但是,在某些情况下这是不切实际的:例如,如果您的 API 公开了大量功能,则将它们全部复制可能是不可行的。

该 net/http 程序包选择了上下文结构方法,该方法提供了有用的案例研究。让我们看一下net/httpDo。在推出context.Context之前,Do被定义为如下:

func (c *Client) Do(req *Request) (*Response, error)

在 Go 1.7 之后,Do 可能不是这样,因为它会破坏向后兼容性:

func (c *Client) Do(ctx context.Context, req *Request) (*Response, error)

但是,保留向后兼容性并遵守Go1 兼容性承诺对于标准库至关重要。因此,维护人员选择在http.Request结构上添加一个context.Context,以便在不破坏向后兼容性的情况下提供context.Context支持:

type Request struct {
  ctx context.Context

  // ...
}

func NewRequestWithContext(ctx context.Context, method, url string, body io.Reader) (*Request, error) {
  // Simplified for brevity of this article.
  return &Request{
    ctx: ctx,
    // ...
  }
}

func (c *Client) Do(req *Request) (*Response, error)

如上说述,当你要改造 API 以支持context时,将context.Context添加到结构中是有意义的。但是,请记住首先考虑复制您的函数,这可以在不牺牲实用性和理解力的情况下向后兼容 context.Context。例如:

func (c *Client) Call() error {
  return c.CallContext(context.Background())
}

func (c *Client) CallContext(ctx context.Context) error {
  // ...
}

总结

通过 Context,可以轻松地在调用堆栈中传播重要的跨库和跨 API 信息。但是,必须一致,清晰地使用它,以使其易于理解,易于调试且有效。

当作为 method中的第一个参数传递而不是存储在 struct 类型中时,用户可以充分利用其可扩展性,以便通过调用堆栈构建强大的取消,截止日期和元数据信息树。而且,最重要的是,当将其作为参数传入时,可以清楚地了解其范围,从而可以在堆栈中上下轻松地理解和调试。

在设计具有 context 的 API 时,请记住以下建议:context.Context作为参数传递;不要将其存储在struct中。

References

  1. context.Context 以及文档 https://golang.org/pkg/context
  2. 您的程序可能会积压 https://sre.google/sre-book/handling-overload
  3. 保持模块兼容 https://blog.golang.org/module-compatibility
  4. Go1 兼容性承诺 https://golang.org/doc/go1compat
更多原创文章干货分享,请关注公众号
  • 加微信实战群请加微信(注明:实战群):gocnio
暂无回复。
需要 登录 后方可回复, 如果你还没有账号请点击这里 注册