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 把 work 添加到远程的服务运行
type Worker struct { /* … */ }

type Work struct { /* … */ }

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

func (w *Worker) Fetch(ctx context.Context) (*Work, error) {
  _ = ctx // 一个提前传入的 ctx 用来控制请求的 deadline、取消以及元数据
}

func (w *Worker) Process(ctx context.Context, work *Work) error {
  _ = ctx // 一个提前传入的 ctx 用来控制请求的 deadline、取消以及元数据
}

在这里 (*Worker).Fetch(*Worker).Process 都直接把 context 作为第一个参数。用户可以为每一次调用设置 deadline、取消和元数据。并且这样可以很清晰的看到 context 在每个方法中的使用方式,这样就不会让传递到一个方法中的 context 会在其他的方法中被调用。这是因为 conetext 的作用域限制到了真正需要它的地方,这样 context 就会实用而清晰。

把 context 存进 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 // 共享的 w.ctx 用来控制请求的 deadline、取消以及元数据
}

func (w *Worker) Process(work *Work) error {
  _ = w.ctx // 共享的 w.ctx 用来控制请求的 deadline、取消以及元数据
}

这里 (*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 之前,它是这样定义的:

// Do 发送 http 请求并且返回 http 的响应
func (c *Client) Do(req *Request) (*Response, error)

在 Go1.17 之后,如果我们不管向后的兼容性,Do 的定义可能是下面这样:

// Do 发送 http 请求并且返回 http 的响应 
func (c *Client) Do(ctx context.Context, req *Request) (*Response, error)

但为了保护兼容性,并且遵守 Go 对标准库兼容性的保证非常重要。所以,维护者选择在 http.Request 中添加一个 context 来保证这个 API 的向后兼容性:


type Request struct {
  ctx context.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 添加到 struct 中。然而,在不破坏代码的可用性和可读性时,为了保证代码的向后兼容,还是应该重新创建一个方法,像下面这样:


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

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

小结

在一个调用栈中,Context 在跨库或者跨 API 传递信息时非常有用。但为了保证可读性、可调试性和有效性,它必须保持简洁和连贯。

当通过参数传递 context 而不是存储在 Context 中时,用户可以完全利用它的扩展性在调用栈中构造一个由取消、deadline 和元数据信息组成的树,并且在通过参数传递时,它们的作用域是非常清晰,这让代码的可读性和可调试性都非常好。

当在设计一个带 context 的 API 时,记住一点:通过参数传递 context,不要把它存在 struct 中。

文 / Rayjun

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,491评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,856评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,745评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,196评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,073评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,112评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,531评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,215评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,485评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,578评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,356评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,215评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,583评论 3 299
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,898评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,174评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,497评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,697评论 2 335

推荐阅读更多精彩内容