Go语言中的上下文取消操作详解

前言

许多使用Go的人,都会用到它的上下文库。大多数使用 context 进行下游操作,比如发出HTTP调用,或者从数据库获取数据,或者在协程中执行异步操作。最常见的用法是传递可由所有下游操作使用的公共数据。然而,一个不太为人所知,但非常有用的上下文特性是,它能够在中途取消或停止一个操作。

本篇文章将解释我们如何利用上下文库的取消特性,并通过一些模式和最佳实践来使用取消,使你的程序更快、更健壮。

为什么需要取消?

简而言之,我们需要取消,以防止我们的系统做不不需要的工作。

考虑HTTP服务器对数据库的调用的常见情况,并将查询的数据返回给客户端:

时间图,如果一切都很完美,就会是这样的:

但是,如果客户端取消了中间的请求,会发生什么呢?例如,如果客户端关闭了他们的浏览器,这可能会发生。如果没有取消,应用服务器和数据库将继续执行它们的工作,即使工作的结果将被浪费:

理想情况下,如果我们知道进程(在本例中是HTTP请求)停止了,我们希望流程的所有下游组件停止工作:

1、上下文取消

现在我们知道了为什么需要取消,让我们来看看如何实现它。因为“取消”的事件与交易或正在执行的操作高度相关,所以它与上下文捆绑在一起是很自然的。

取消的有两个方面,你可能想要实现:

  • 监听取消事件
  • 提交取消事件

2、监听取消事件

上下文类型提供了 Done() 方法,每当上下文收到取消事件时,它都会返回接收空 struct{} 类型的通道。监听取消事件就像等待 <-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")
 }
 }))
}

你可以通过运行服务器并在浏览器上打开localhost:8000来测试。如果你在2秒前关闭浏览器,你应该会看到在终端窗口上打印的“请求取消”。

3、提交取消事件

如果你有一个可以被取消的操作,你将不得不通过上下文发出取消事件。这可以通过 context 包中的 WithCancel 函数来完成,它返回一个上下文对象和一个函数。这个函数没有参数,也不返回任何东西,当你想要取消上下文时调用。

考虑两个从属操作的情况。在这里,“依赖”意味着如果一个失败了,另一个就没有意义了。在这种情况下,如果我们在早期就知道其中一个操作失败了,我们想要取消所有的依赖操作。

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)
}

4、基于时间取消

任何需要在请求的最大持续时间内维护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

你可以使用超时来实现上述两个结果。

陷阱和警告

尽管Go的上下文取消是一个通用的工具,但是在继续之前,有一些事情是你应该记住的。其中最重要的一点是, 上下文只能被取消一次 。

如果你想在同一个操作中提出多个错误,那么使用上下文取消可能不是最好的选择。使用取消的最惯用的方法是,当你真正想要取消某些东西时,而不仅仅是通知下游进程,错误已经发生了。

你需要记住的另一件事是,相同的上下文实例应该传递给所有你可能想要取消的功能和例程。用 WithTimeout 或 WithCancel 来包装已经可取消的上下文将会导致多种可能性,你的上下文可以被取消,并且应该避免。

总结

以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作具有一定的参考学习价值,如果有疑问大家可以留言交流,谢谢大家对我们的支持。

(0)

相关推荐

  • GO语言(golang)基础知识

    今天说一些golang的基础知识,还有你们学习会遇到的问题,先讲解hello word 复制代码 代码如下: package main import "fmt" func main() {    fmt.Println("你好,我们"); } package name 包机制,每一个独立的go程序都需要有一个package main的申明,主要是要为下边入口函数main()做申明的,import和java一样导入包用的 就是下边我们函数用的fmt.Println()

  • Go语言运行环境安装详细教程

    Go的三种安装方式 Go有多种安装方式,你可以选择自己喜欢的.这里我们介绍三种最常见的安装方式: 1.Go源码安装:这是一种标准的软件安装方式.对于经常使用Unix类系统的用户,尤其对于开发者来说,从源码安装是最方便而熟悉的. 2.Go标准包安装:Go提供了方便的安装包,支持Windows.Linux.Mac等系统.这种方式适合初学者,可根据自己的系统位数下载好相应的安装包,一路next就可以轻松安装了. 3.第三方工具安装:目前有很多方便的第三方软件包工具,例如Ubuntu的apt-get.M

  • Golang极简入门教程(一):基本概念

    安装 Golang 在 http://golang.org/dl/ 可以下载到 Golang.安装文档:http://golang.org/doc/install. Hello Go 我们先创建一个文件 hello.go: 复制代码 代码如下: package main   import "fmt"   func main() {     fmt.Printf("hello Golang\n"); } 执行此程序: 复制代码 代码如下: go run hello.g

  • Go语言命令行操作命令详细介绍

    Go 命令 Go语言自带有一套完整的命令操作工具,你可以通过在命令行中执行go来查看它们: 图1.3 Go命令显示详细的信息 这些命令对于我们平时编写的代码非常有用,接下来就让我们了解一些常用的命令. go build 这个命令主要用于测试编译.在包的编译过程中,若有必要,会同时编译与之相关联的包. 1.如果是普通包,就像我们在1.2节中编写的mymath包那样,当你执行go build之后,它不会产生任何文件.如果你需要在$GOPATH/pkg下生成相应的文件,那就得执行go install了

  • Golang极简入门教程(四):编写第一个项目

    workspace Golang 的代码必须放置在一个 workspace 中.一个 workspace 是一个目录,此目录中包含几个子目录: 1.src 目录.包含源文件,源文件被组织为包(一个目录一个包) 2.pkg 目录.包含包对象(package objects) 3.bin 目录.包含可执行的命令 包源文件(package source)被编译为包对象(package object),命令源文件(command source)被编译为可执行命令(command executable).

  • 在Django的上下文中设置变量的方法

    前一节的例子只是简单的返回一个值. 很多时候设置一个模板变量而非返回值也很有用. 那样,模板作者就只能使用你的模板标签所设置的变量. 要在上下文中设置变量,在 render() 函数的context对象上使用字典赋值. 这里是一个修改过的 CurrentTimeNode ,其中设定了一个模板变量 current_time ,并没有返回它: class CurrentTimeNode2(template.Node): def __init__(self, format_string): self.

  • Go语言中的上下文取消操作详解

    前言 许多使用Go的人,都会用到它的上下文库.大多数使用 context 进行下游操作,比如发出HTTP调用,或者从数据库获取数据,或者在协程中执行异步操作.最常见的用法是传递可由所有下游操作使用的公共数据.然而,一个不太为人所知,但非常有用的上下文特性是,它能够在中途取消或停止一个操作. 本篇文章将解释我们如何利用上下文库的取消特性,并通过一些模式和最佳实践来使用取消,使你的程序更快.更健壮. 为什么需要取消? 简而言之,我们需要取消,以防止我们的系统做不不需要的工作. 考虑HTTP服务器对数

  • Go语言中的数据竞争模式详解

    目录 前言 Go在goroutine中通过引用来透明地捕获自由变量 切片会产生难以诊断的数据竞争 并发访问Go内置的.不安全的线程映射会导致频繁的数据竞争 Go开发人员常在pass-by-value时犯错并导致non-trivial的数据竞争 消息传递(通道)和共享内存的混合使用使代码变得复杂且易受数据竞争的影响 Add和Done方法的错误放置会导致数据竞争 并发运行测试会导致产品或测试代码中的数据竞争 小结 前言 本文主要基于在Uber的Go monorepo中发现的各种数据竞争模式,分析了其

  • Java语言中的内存泄露代码详解

    Java的一个重要特性就是通过垃圾收集器(GC)自动管理内存的回收,而不需要程序员自己来释放内存.理论上Java中所有不会再被利用的对象所占用的内存,都可以被GC回收,但是Java也存在内存泄露,但它的表现与C++不同. JAVA中的内存管理 要了解Java中的内存泄露,首先就得知道Java中的内存是如何管理的. 在Java程序中,我们通常使用new为对象分配内存,而这些内存空间都在堆(Heap)上. 下面看一个示例: public class Simple { public static vo

  • C语言中的正则表达式使用示例详解

    正则表达式,又称正规表示法.常规表示法(英语:Regular Expression,在代码中常简写为regex.regexp或RE).正则表达式是使用单个字符串来描述.匹配一系列符合某个句法规则的字符串. 在c语言中,用regcomp.regexec.regfree 和regerror处理正则表达式.处理正则表达式分三步: 编译正则表达式,regcomp: 匹配正则表达式,regexec: 释放正则表达式,regfree. 函数原型 /* 函数说明:Regcomp将正则表达式字符串regex编译

  • C语言中递归和排列组合详解

    目录 排列组合三大问题: 1.打印n个数的全排列 2.打印n个数中任意m个数的全排列 3.打印n个数中任意m个数的组合 完整代码如下: 总结 排列组合三大问题: 1.打印n个数的全排列2.打印n个数中任意m个数的全排列3.打印n个数中任意m个数的组合 1.打印n个数的全排列 这个题实际上是可以直接用STL中的next_permutation()函数,代码如下: #include<bits/stdc++.h> using namespace std; int main(){ int data[4

  • C语言数据结构之单链表操作详解

    目录 1.插入操作 2.删除操作 3.查找操作 4.修改操作 5.完整代码 1.插入操作 (1)创建一个新的要插入的结点 (2)将新结点的 next 指针指向插入位置后的结点 (3)将插入位置前的节点指针 next 指向新的结点 注意:步骤(2)(3)的顺序不能颠倒,否则会导致插入位置后的部分链表丢失. 插入位置一共分三种,分别是头部插入.中间插入和尾部插入. 如图: 代码: link* insertElem(link* p,int elem,int pos){ link* temp = p;/

  • Kotlin 语言中调用 JavaScript 方法实例详解

    Kotlin 语言中调用 JavaScript 方法实例详解 Kotlin 已被设计为能够与 Java 平台轻松互操作.它将 Java 类视为 Kotlin 类,并且 Java 也将 Kotlin 类视为 Java 类.但是,JavaScript 是一种动态类型语言,这意味着它不会在编译期检查类型.你可以通过动态类型在 Kotlin 中自由地与 JavaScript 交流,但是如果你想要 Kotlin 类型系统的全部威力 ,你可以为 JavaScript 库创建 Kotlin 头文件. 内联 J

  • C语言中联合体union的实例详解

     C语言中联合体union的实例详解 1.定义: union(int i, short s, char c) un; un.i = 3; printf("i=%d",un.i); printf("length = %d\n",sizeof(un);//==4,有最大的变量来决定 2.相当与java里的List T类型 3.数据交换 void swap(int *p , int *q){ int temp = *p; *p = *q; *q = temp; } 4.打

  • C语言中二级指针的实例详解

    C语言中二级指针的实例详解 用图说明 示例代码: #include <stdio.h> int main(int argc, const char * argv[]) { // int a = 5; int *p1 = &a; //-打印地址-----地址相同--------------- printf("&a = %p\n", &a);// printf("p1 = %p\n", p1);// int **p2 = &p

  • C语言中调用Swift函数实例详解

    C语言中调用Swift函数实例详解 在Apple官方的<Using Swift with Cocoa and Objectgive-C>一书中详细地介绍了如何在Objective-C中使用Swift的类以及如何在Swift中使用Objective-C中的类.在后半部分也介绍了如何在Swift中使用C函数,不过对于如何在C语言中使用Swift函数却只字未提.这里我就为大家分享一下如何在C语言中调用Swift函数. 我们首先要知道的是,所有Swift函数都属于闭包.其次,Swift函数的调用约定与

随机推荐