ICode9

精准搜索请尝试: 精确搜索
首页 > 其他分享> 文章详细

使用 Go 语言 context 包的取消功能

2021-07-01 09:53:55  阅读:154  来源: 互联网

标签:http 取消 request ctx context time 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 请求,从数据获取数据并将数据返回给客户端。

watermark,size_16,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_90,type_ZmFuZ3poZW5naGVpdGk=

如果一切正常的话,时序图应该是下面这样的:

watermark,size_16,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_90,type_ZmFuZ3poZW5naGVpdGk=

但是,如果客户端中途取消了请求会发生什么?类似的场景,比如:关闭了浏览器等。如果不取消操作,服务器和数据库仍然会继续完成执行,即使它们的执行成果会被浪费:

watermark,size_16,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_90,type_ZmFuZ3poZW5naGVpdGk=

理想情况下,如果我们知道请求中断了,我们希望该请求下游的所有工作组件都停止执行。

watermark,size_16,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_90,type_ZmFuZ3poZW5naGVpdGk=

Context cancellation in Go

现在我们已经知道为什么需要取消,接着就来看看如何实现。因为取消事件与事务和操作高度相关,所以很自然将它与 context 联系在一起。

取消主要有两个方面:

  1. 监听取消事件;

  2. 发出取消事件;

监听取消事件

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 篇优质文章。
如果你有想交流的话题,欢迎留言。


如果我的文章对你有所帮助,点赞、转发都是一种支持!watermark,size_16,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_90,type_ZmFuZ3poZW5naGVpdGk=

标签:http,取消,request,ctx,context,time,Go
来源: https://blog.51cto.com/u_15289640/2962213

本站声明: 1. iCode9 技术分享网(下文简称本站)提供的所有内容,仅供技术学习、探讨和分享;
2. 关于本站的所有留言、评论、转载及引用,纯属内容发起人的个人观点,与本站观点和立场无关;
3. 关于本站的所有言论和文字,纯属内容发起人的个人观点,与本站观点和立场无关;
4. 本站文章均是网友提供,不完全保证技术分享内容的完整性、准确性、时效性、风险性和版权归属;如您发现该文章侵犯了您的权益,可联系我们第一时间进行删除;
5. 本站为非盈利性的个人网站,所有内容不会用来进行牟利,也不会利用任何形式的广告来间接获益,纯粹是为了广大技术爱好者提供技术内容和技术思想的分享性交流网站。

专注分享技术,共同学习,共同进步。侵权联系[81616952@qq.com]

Copyright (C)ICode9.com, All Rights Reserved.

ICode9版权所有