The Go Blog: 关于 context 的一点最佳实践
关于 context 的一点最佳实践
2021 年 2月 24 日,官方 blog 中详细讲了关于 context 使用的一些最佳实践,提供了代码示例,告诉你为何 context不应存储在 struct 内部,最好的方式是作为函数的第一个参数传递
,以及如何在非常必要的情况下(保持向后兼容)以一种最安全的方式将 context 存储到 struct 中。下面是原文的主要内容。
Introduction
在许多 Go API,尤其是现代 API 中,函数和方法的第一个参数通常是 context.Context
。context
提供了很多方法例如 WithCancel
、WithDeadline
、WithValue
、以实现跨 API 的流程控制。很多 lib 在与远程服务器(如数据库、api等)交互时,经常使用 context
做控制。
context 的文档中讲到:
context 不应存储在 struct 内部,最好的方式是作为函数的第一个参数传递
本文在此建议的基础上讲解了原因和示例,说明了为什么要使用函数传递 Context
而不是将其存储在 struct 中的重要性。还以 net/http
的代码为例,解释了应该在何种情况下可以在 struct 中 存储 Context
。
推荐 context 作为函数参数传递
让我们先看下在函数中传递 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, w *Work) error {
_ = ctx // A per-call ctx is used for cancellation, deadlines, and metadata.
}
这里 (*Worker).Fetch
和 (*Worker).Process
都直接将 context
作为函数第一个参数。这样从 context
的生成到结束,调用方可以很清晰地知道 context
的传递路线。
将 context 存储到 strcut 所带来的一些困惑
在结构体中嵌套 context
来实现上面的 Worker
示例,调用者对所使用的 context
生命周期产生迷惑:
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(w *Work) error {
_ = w.ctx // A shared w.ctx is used for cancellation, deadlines, and metadata.
}
(*Worker).Fetch
和 (*Worker).Process
方法同时使用了 Worker
结构体中的 context
,这种情况下使得调用方无法定义不同的 context
,比如有调用方想用 WitchCancel
,有的想用 WithDeadline
,也很难理解上面传来的 context 的作用是 cancel
?还是 deadline?
调用者所使用 context
的生命周期被绑定到了一个共享的 context 上面。
特殊情况:保留向后兼容性
当 go 1.7 版本发布时,大量的的 API 需要以向后兼容的方式支持 context.Context,例如,net/http
的 Client
方法(例如Get和Do)是使用 context 的典范。使用这些方法发送的 http 请求都将受益于 context.Context
附带的 WithDeadline
,WithCancel
和 WithValue
等方法支持。
一般有两种方式能够在支持 context.Context
的同时保持代码的向后兼容:
- 在 struct 中添加 context (稍后我们将看到);
- 复制原有函数,在函数第一个参数中使用 context,举个栗子,
database/sql
这个 package 的Query
方法的签名一直是:
func (db *DB) Query(query string, args ...interface{}) (*Rows, error)
当 context
package 引入的时候,Go team 新增了这样一个函数:
func (db *DB) QueryContext(ctx context.Context, query string, args ...interface{}) (*Rows, error)
并且只修改了一处代码:
func (db *DB) Query(query string, args ...interface{}) (*Rows, error) {
return db.QueryContext(context.Background(), query, args...)
}
通过这种方式,Go team 能够在平滑地升级一个 package
的同时不对代码的可读性、兼容性造成影响。类似的代码在 golang 源码中随处可见。更多的保持代码兼容性的讨论可见 [Go team 关于如何保持 Go Modules 兼容性的一些实践]。
然而在某些情况下,比如 API 公开了大量 function,重写所有函数是不切实际的。
package net/http
选择在 struct 中添加 context.Context,这是一个结构体嵌套 context 比较恰当的范例。先让我们看下 net/http
的 Do
函数。在引入 context
之前,Do
的定义如下:
func (c *Client) Do(req *Request) (*Response, error)
在 1.7 引入 context
后,为了遵循 net/http
这种标准库的向后兼容原则,考虑到该核心库所包含的函数过于多,maintainers 选择在结构体 http.Request
中添加 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
进行支持,在不牺牲实用性和理解性的前提下向后兼容上下文:
func (c *Client) Call() error {
return c.CallContext(context.Background())
}
func (c *Client) CallContext(ctx context.Context) error {
// ...
}
Conclusion
通过 context
,可以轻松地在调用堆栈中传播重要的跨 lib 和跨 API 信息。但是,必须一致、清晰地使用它,以使其易于理解,易于调试且有效。
当 context
作为方法中的第一个参数传递而不是存储在 struct 中时,用户可以充分利用其可扩展性,可以通过调用堆栈构建 WithCancel
,WithDeadline
和 WithValue
的传递树。而且最重要的是,当将其作为参数传入时,可以清楚地了解其传播范围,从而可以轻松地理解和调试。
最后一句话总结本文,When designing an API with context, remember the advice: pass context.Context
in as an argument; don't store it in structs.
原文地址:https://blog.golang.org/context-and-structs
官方资讯*最新技术*独家解读