Context 和 struct
原文地址:https://go.dev/blog/context-and-structs
在很多 Go 的 API 中,特别是新的 API,函数或者方法的第一个参数通常是 context.Context
。context.Context 可以在不同的 API 之间传递一些信号,比如 deadline、调用者的取消信号,也可以传递一些请求范围内的数据。在一个库需要直接或者间接的与数据库、远程 API 等等远程服务进行交互的时候会用到。
在 Context 的文档中这样说到:context 只应该在每个函数需要用到它的时候传递,而不应该存储在 struct 中。
这篇文章会通过一些例子来说明为什么应该直接传递 context,而不是把它存储在另一个类型中。同时也会介绍一个把 context 安全存储在 struct 中的少见案例,并会解释为什么要这么做。
把 context 作为参数
我们先来看一个把 context 作为参数的例子,来看看把 context 当做参数传递传递的优点:
在这里 (*Worker).Fetch
和 (*Worker).Process
都直接把 context 作为第一个参数。用户可以为每一次调用设置 deadline、取消和元数据。并且这样可以很清晰的看到 context 在每个方法中的使用方式,这样就不会让传递到一个方法中的 context 会在其他的方法中被调用。这是因为 conetext 的作用域限制到了真正需要它的地方,这样 context 就会实用而清晰。
把 context 存进 struct 会造成误解
我们再看一下上面的例子,并做一点小改动,把 context 放进 struct 中。这样做问题在于这样会让调用者的生命周期变得模糊,或者会把这两者的作用域混在一起,这样更糟糕。
这里 (*Worker).Fetch
和 (*Worker).Process
共用一个存储在 Worker 中的 context。这将会让 Fetch 和 Process 的调用者无法指定 deadline、取消请求或者获取元数据,因为一个请求中可能有不同的 context。举个例子来说,无法只为 (*Worker).Fetch 指定 deadline,也无法只取消 (*Worker).Process。调用者的生命周期被一个共享的 context 打乱了,而且 context 的生命周期与 Worker 的生命周期相同。
相比于之前的那种写法,这种更容易让人疑惑。用户可能会问他们自己:
在创建一个新的 context 的时候,怎么知道后面是需要取消请求还是设置一个 deadline
这个 context 能不能在 (*Worker).Fetch 和 (*Worker).Process 继续传递,两个都不能?还是一个可以,一个不行
在这个 API 中,我们就需要在文档中明确的告诉用户这里的 context 是用来做什么的。用户可能得通过阅读代码,而不是直接通过 API 的结构来判断 context 的用途。
例外:向后保持兼容性
当 Go1.17 发布的时候,大量的 API 需要添加 context 以保证 API 的向后兼容性。比如 net/http 中的 Client 方法,Get、Do 都需要添加 context。每一个通过这些方法发送的外部请求,都可以通过 context 来传递 deadline、取消请求、传递元数据。
这里有两种可以保持向后兼容的方式来添加对 context 的支持:第一个方法就是把 context 放在一个 struct 中,就像我们前面看到的那样,另一种方法就是重新写一个不同名称的方法,添加 context 参数。就像我们在如何保证模块的兼容性中讨论的那样,第二种方法应该要优于第一种方法。但是在一些情况下,是无法这样实现的:比如你的 API 暴露了大量的方法,然后把它们全部都重写一遍,这样可能会让代码很混乱。
net/http 包选则了第一种方法,这里也提供了一个很值得学习的例子。我们来看一下其中的 Do 方法,在添加 context 之前,它是这样定义的:
在 Go1.17 之后,如果我们不管向后的兼容性,Do 的定义可能是下面这样:
但为了保护兼容性,并且遵守 Go 对标准库兼容性的保证非常重要。所以,维护者选择在 http.Request 中添加一个 context 来保证这个 API 的向后兼容性:
当你在为你的 API 添加对 context 的支持时,可以选择把 context 添加到 struct 中。然而,在不破坏代码的可用性和可读性时,为了保证代码的向后兼容,还是应该重新创建一个方法,像下面这样:
小结
在一个调用栈中,Context 在跨库或者跨 API 传递信息时非常有用。但为了保证可读性、可调试性和有效性,它必须保持简洁和连贯。
当通过参数传递 context 而不是存储在 Context 中时,用户可以完全利用它的扩展性在调用栈中构造一个由取消、deadline 和元数据信息组成的树,并且在通过参数传递时,它们的作用域是非常清晰,这让代码的可读性和可调试性都非常好。
当在设计一个带 context 的 API 时,记住一点:通过参数传递 context,不要把它存在 struct 中。
文 / Rayjun
版权声明: 本文为 InfoQ 作者【Rayjun】的原创文章。
原文链接:【http://xie.infoq.cn/article/a943bddb6b422c397032588da】。文章转载请联系作者。
评论