点击上方蓝色“Golang来啦”关注我哟
加个“星标”,天天 15 分钟,掌握 Go 语言
via:
https://www.sohamkamani.com/golang/2018-06-17-golang-using-context-cancellation/
作者:Soham Kamani
大家好,我是 Seekload。
有些文章会涉及到很多第三方资源,比如代码地址、与文章内容相关的其他优质阅读源等,但微信公众号文章不支持外部链接,就很尴尬。为了方便大家阅读,以后文章会同步至四哥的博客。
因为之前没怎么宣传,应该很少有人知道四哥的博客地址:
seekload.net
应该很容易记住,点击文末的【阅读原文】直达。
开始今天的文章,原文如下:
使用过 Go 语言的人对 context 包应该都不陌生。context 包经常用于需要执行一些下游操作的地方,比如:执行 HTTP 请求、从数据库获取数据或者使用协程执行异步操作。最普通的用法就是向下游传递一些公共数据。然而,一个 context 鲜为人知但是非常有用的功能就是可以用于中途取消或者停止操作。
接下来的内容我们将会讨论如何使用 context 提供的取消功能,并提供一些最佳实践供参考,为你编写效率更高、代码更健壮的程序提供借鉴。
为什么我们需要取消?
简而言之,取消是为了防止系统做一些不必要的工作。
我们拿一种常见的场景举例,比如:用户发出 HTTP 请求,从数据获取数据并将数据返回给客户端。
如果一切正常的话,时序图应该是下面这样的:
但是,如果客户端中途取消了请求会发生什么?类似的场景,比如:关闭了浏览器等。如果不取消操作,服务器和数据库仍然会继续完成执行,即使它们的执行成果会被浪费:
理想情况下,如果我们知道请求中断了,我们希望该请求下游的所有工作组件都停止执行。
Context cancellation in Go
现在我们已经知道为什么需要取消,接着就来看看如何实现。因为取消事件与事务和操作高度相关,所以很自然将它与 context 联系在一起。
取消主要有两个方面:
监听取消事件;
发出取消事件;
监听取消事件
Context 类型提供了 Done() 方法,每次 context 接收到取消事件时,该方法都是返回一个 channel,这个 channel 会收到空结构体类型的数据。监听取消事件也很容易,<- ctx.Done()。
比如,一个 HTTP 请求处理需要两秒,如果在中途取消就必须立即返回。
func main() {
// Create an HTTP server that listens on port 8000
http.ListenAndServe(":8000", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
// This prints to STDOUT to show that processing has started
fmt.Fprint(os.Stdout, "processing request\n")
// We use `select` to execute a peice of code depending on which
// channel receives a message first
select {
case <-time.After(2 * time.Second):
// If we receive a message after 2 seconds
// that means the request has been processed
// We then write this as the response
w.Write([]byte("request processed"))
case <-ctx.Done():
// If the request gets cancelled, log it
// to STDERR
fmt.Fprint(os.Stderr, "request cancelled\n")
}
}))
}
你可以使用 go run 将服务跑起来,在浏览器中打开 localhost:8000,如果你在 2s 钟之内关闭浏览器,终端将会输出 request cancelled。
发出取消事件
如果你有需要取消的操作,可以通过 context 发出取消事件。可以通过 context 包提供的 WithCancel 函数完成,该函数返回 context 对象和一个取消函数,这个函数不带任何参数、没有返回值,当你需要取消 context 时可以调用该函数。
假设有两个相互依赖的操作,这里“依赖”的意思是,如果其中一个操作失败了,另一条操作即使完成也没有任何意义。这个场景里,如果我们事先知道其中一个操作失败了,我们需要取消所有的操作。
func operation1(ctx context.Context) error {
// Let's assume that this operation failed for some reason
// We use time.Sleep to simulate a resource intensive operation
time.Sleep(100 * time.Millisecond)
return errors.New("failed")
}
func operation2(ctx context.Context) {
// We use a similar pattern to the HTTP server
// that we saw in the earlier example
select {
case <-time.After(500 * time.Millisecond):
fmt.Println("done")
case <-ctx.Done():
fmt.Println("halted operation2")
}
}
func main() {
// Create a new context
ctx := context.Background()
// Create a new context, with its cancellation function
// from the original context
ctx, cancel := context.WithCancel(ctx)
// Run two operations: one in a different go routine
go func() {
err := operation1(ctx)
// If this operation returns an error
// cancel all operations using this context
if err != nil {
cancel()
}
}()
// Run operation2 with the same context we use for operation1
operation2(ctx)
}
基于时间的取消事件
任何应用程序都需要在超时时间之内维护 SLA 可用性,可以采用基于时间的取消事件。相关的 API 与上面提到的例子类似,但是有一点补充:
// The context will be cancelled after 3 seconds
// If it needs to be cancelled earlier, the `cancel` function can
// be used, like before
ctx, cancel := context.WithTimeout(ctx, 3*time.Second)
// The context will be cancelled on 2009-11-10 23:00:00
ctx, cancel := context.WithDeadline(ctx, time.Date(2009, time.November, 10, 23, 0, 0, 0, time.UTC))
例如,使用 HTTP API 调用外部服务,如果请求时间太长,最好尽早取消请求。
func main() {
// Create a new context
// With a deadline of 100 milliseconds
ctx := context.Background()
ctx, _ = context.WithTimeout(ctx, 100*time.Millisecond)
// Make a request, that will call the google homepage
req, _ := http.NewRequest(http.MethodGet, "http://google.com", nil)
// Associate the cancellable context we just created to the request
req = req.WithContext(ctx)
// Create a new HTTP client and execute the request
client := &http.Client{}
res, err := client.Do(req)
// If the request failed, log to STDOUT
if err != nil {
fmt.Println("Request failed:", err)
return
}
// Print the statuscode if the request succeeds
fmt.Println("Response received, status code:", res.StatusCode)
}
输出的结果取决于请求谷歌主页的快慢,有可能输出:
Response received, status code: 200
或者
Request failed: Get http://google.com: context deadline exceeded
陷阱和警告
尽管 Context 的取消功能是一个很好用的工具,但是使用时有一些需要主要的点。最重要的是,context 只能被取消一次。如果你希望在同一操作中传递多个错误,那么使用 context 取消可能不是最佳选择。使用取消最常见的场景是仅仅希望取消操作,而不是返回下游操作出现的错误。
需要注意的另一点就是,应将相同的 context 对象传递给可能要取消的所有函数或者协程,使用 WithTimeout 或 WithCancel 包装一个已经可取消的 context 将导致多种可能的上下文被取消,应该避免。
ps: 文章完整代码地址
https://github.com/sohamkamani/blog-example-go-context-cancellation
这是持续翻译的第 18/100 篇优质文章。
如果你有想交流的话题,欢迎留言。
推荐阅读:
对了,看完文章,记得点击下面的卡片,关注我哦~
如果我的文章对你有所帮助,点赞、在看、转发都是一种支持!