diff --git a/chapter-37-Context/chapter_37.md b/chapter-37-Context/chapter_37.md new file mode 100644 index 0000000000000000000000000000000000000000..5bfc86bf95eed211cfd1f5f7da6414bff5190632 --- /dev/null +++ b/chapter-37-Context/chapter_37.md @@ -0,0 +1,1169 @@ +# Chapter 37: Context + +![Context](imgs/context.92f43817.jpg) + +## 1. 你将在本章学到什么 + +* 什么是 context (上下文)? +* 什么是链表 ? +* 如何使用 context (上下文)包 。 + +## 2. 涵盖的技术概念 + +* 上下文推导 +* 链表列表 +* Context key-value pair (上下文键值对) +* 取消 +* 超时 +* 最后期限 + +## 3. 简介 + +这一章专门讨论上下文包。在本章的第一部分,我们将发现什么是 "上下文 "以及它的目的是什么。在第二部分,我们将看到如何在真实的程序中使用上下文包 + +## 4. 什么是面向上下文的编程 ? + +### 4.1 定义 + +上下文来自拉丁词“contexo”。 它意味着将某物与一组其他事物联合、连接、链接。 我们在这里有这样的想法,即事物的上下文是一组与其他事物的链接和联系。 在日常语言中,我们使用如下表达式: + +* Take something out of context. + +* 在某事的上下文中。 + +事物、动作、词语都有上下文,与其他事物有联系。 如果我们从它的上下文中剔除某些东西,我们就会把它简化为我们可能会误解的东西。 上下文是改进决策的信息的集合。 什么是上下文的一部分? 这是部分列表: + +* 地点 + +* 日期 + +* 历史 + +* 人 + + +为了更好地理解为什么 context(上下文)对我们很重要,让我们举一些例子。 + +### 4.2 上下文增加对事件的理解 +想象一下,在散步时,您听到两个人之间的对话: + +Alice +你看过上周的比赛吗? + +Bob +是的! + +Alice +在此之后,我相信他们会赢得下一个! + +Bob +当然,我会赌一千 + +他们谈论“比赛”。一支球队上周赢得了一场比赛,并且有很多机会在下周赢得另一场比赛。我们不知道它是哪支球队和哪种运动。 + +对话的上下文可以帮助我们理解它。如果谈话发生在纽约,我们可以猜测它与棒球或篮球有关,因为这些运动在那里很受欢迎。如果这次谈话发生在巴黎,那么他们谈论足球的可能性就很高。 + +我们在这里所做的是添加上下文来理解某些东西。在这里,我们谈到了这个地方。我们还可以在对话的上下文中添加时间因素。如果我们知道它发生的时间,我们将能够浏览本周的运动成绩以更好地了解。 + +### 4.3 上下文改变行为 + +对事件上下文的分析将改变参与者的行为。 试着回答其中的一些问题: + +* 你在自己的国家比在其他国家是否更有礼貌? +* 你们在办公室和家人使用相同的语言水平吗? +* 你每天去面试的时候都穿得像这样吗? + +对这三个问题的回答可能是“否”。那是因为上下文。我们根据上下文采取不同的行动。背景正在影响我们的行为。环境会影响我们的行为和反应。 + +### 4.4 计算机科学背景 + +通常情况下,我们设计计算机程序是为了执行一个预定义的任务。我们实现的指定程序总是以同样的方式执行。程序不因使用它的用户而改变。当环境改变时,它的行为也不会改变。 + +面向上下文编程的理念是在程序中引入受情境影响的变化。Abowd在1999年给出了一个有趣的上下文定义:"上下文是我们可以用来描述一个实体的情况的任何信息。一个实体是指被认为与用户和应用程序之间的交互有关的人、地方或物体,包括用户和应用程序本身"。 + +隐式和显式信息是上下文的构建建块。程序员应该考虑上下文来构建可以在运行时调整其行为的应用程序。 + +智能是什么意思?"智能 "这个词来自拉丁词根 "intellego",意思是辨别、理解、注意、意识。如果一个东西能够辨别和理解,它就是智能的。在应用程序中引入上下文并不能使它们变得智能,但它们往往能使它们意识到它们的环境和它们的用户。 + +## 5. "Context " 包:历史和使用案例 + +### 5.1 软件包历史 + +该软件包最初由 Google 开发人员在内部开发。它已在 Go 的标准库中引入。在此之前,它在 Go 子存储库中可用。 + +### 5.2 用途 + +上下文包有两个主要用法: + +#### 5.2.0.1 取消传播 + +为了理解这种用法,让我们以一家名为FooBar的虚构建筑公司为例。 + +巴黎市的使命是建立一个巨大的游泳池。巴黎市长在民众代表中捍卫了自己的想法,该项目已获得批准。公司开始从事该项目;项目经理已经订购了建造游泳池所需的所有原材料。四个月过去了,但市长换了,项目被取消了! + +FooBar的项目经理很生气;该公司必须取消156个订单。他开始通过电话一个接一个地加入他们。他们中的一些人还从其他建筑公司订购了原材料。每个人都在遭受这种快速情况演变的困扰。 + +现在让我们假设项目经理不会取消分包商的订单。其他公司将生产所需的商品,但不会得到报酬。这是对资源的严重浪费。 + +正如您在图 [1](#fig:Cancellation-propagation) 中所想象的那样,项目的取消正在传播给间接参与的所有工作人员。市议会取消该项目;FooBar公司也取消了对承包商的订单。 + +在建筑和其他人类活动中,我们总有办法取消工作。我们可以将取消政策引入我们的程序和上下文包。当向Web服务器发出请求时,如果客户端已断开连接,我们可以取消所有工作链! + +![Cancellation propagation[fig:Cancellation-propagation]](imgs/cancellation_propagation.7f4e0613.png) + + + +#### 5.2.0.2 传输请求作用域的数据以及调用堆栈 + +当向网络服务器发出请求时,负责处理请求的网络服务器函数不会单独完成这项工作。该请求将通过一连串的函数和方法,然后再发送响应。在微服务架构中,一个单一的请求可以产生对其他微服务的新请求!这就是微服务。这个函数调用链就是 "调用栈"。我们将在本节中看到,为什么与调用栈一起传输数据会很有用。 + +我们将举另一个例子:为一个购物应用程序开发一个网络服务器。我们有一个与我们的应用程序互动的用户。 + +* 用户将用其网络浏览器进入登录页面 +* 填写它的登录信息 +* 网络浏览器将向服务器发送一个认证请求,服务器将把请求转发给认证服务。 +* 服务器将建立 "我的账户 "页面(例如通过一个模板),并发送用户的响应。 +* 如果用户请求 "最后的订单 "页面,那么服务器将需要调用订单服务来检索它们。 + +![](imgs/request_context.77c1b04c.png) + +我们可以将哪些数据添加到我们的上下文中? + +* 我们可以将发送请求的设备类型保留在上下文中。 + * 如果设备是手机,我们可以选择加载一个轻量级的模板来改善用户体验。 + * 订单服务也可以只加载最后五个订单,以减少页面的渲染时间。 +* 我们可以将经过身份验证的用户的 id 保存到上下文中。 +* 我们也可以保留传入请求的IP。 + * 身份验证层可以使用它来阻止可疑活动(引入阻止列表、检测错误、多次登录尝试) +* 另一个非常常见的用例是生成单个请求 ID。 requestId 被传递到应用程序的每一层。 使用该 ID,负责维护的团队将能够在日志中跟踪请求。 + +#### 5.2.0.3 设置截止日期和超时 + +截止日期是任务应该完成的时间。 超时是一个非常相似的概念。 我们不考虑日历中的精确日期和时间,而是考虑最大允许持续时间。 我们可以使用上下文来定义长时间运行的进程的时间限制。 这是一个例子: + +* 你开发了一个服务器,而你的客户端有一个指定的超时时间为1秒。 +* 你可以为一个上下文设置1秒的超时;在这个时间段之后,你知道客户端将放弃连接。 +* 在这种情况下,我们同样希望避免浪费资源。 + +### 5.3 Context(上下文)接口 + +上下文包暴露了一个由四个方法组成的接口。 + +```go +type Context interface { + Deadline() (deadline time.Time, ok bool) + Done() <-chan struct{} + Err() error + Value(key interface{}) interface{} +} +``` + +在下一节,我们将看到如何使用软件包 + +## 6 链接列表 + +上下文包是用一个标准的数据结构构建的:链表。为了充分了解上下文的工作原理,我们首先需要了解链表。 + +链接列表是一个数据元素的集合。存储在列表中的数据类型不受限制;它可以是整数、字符串、结构体、浮点数......等等。列表中的每个元素都是一个节点。每个节点包含两个东西。 + +* 数据值 +* 列表中下一个元素在内存中的地址。换句话说,这是一个指向下一个值的指针。 + +你可以在图3中看到一个链接列表的直观表示。 + +列表是 "链接的",列表中的节点有一个子(列表中的下一个元素)和一个父(列表中的最后一个元素)。请注意,这个说法并不正确;列表中的第一个节点没有父节点。它是根,是原点,是列表的头。还有一个值得注意的例外,最后一个节点没有任何子节点。 + +![Linked List[fig:Linked-List]](imgs/linked_list.2f868379.png) + +​ Linked List[fig:Linked-List] + +![Linked List : pointers and values[fig:Linkedl-pointers-and-values]](imgs/linked_list_pointers.3614379a.png) + +​ Linked List : pointers and values[fig:Linkedl-pointers-and-values] + +## 7 root context: Background + +在大多数程序中,我们在程序的根部创建一个Background概念。例如,在将启动我们的应用程序的主函数中。要创建根上下文,你可以使用以下语法。 + +```go +ctx := context.Background() +``` + +对Background()函数的调用将返回一个指向空上下文的指针。在内部,对Background()的调用将创建一个新的context.emptyCtx。 + +这种类型没有对外暴露出来: + +```go +type emptyCtx int +``` + +emptyCtx的基本类型是int。这个类型实现了Context接口所要求的四个方法。 + +```go +func (*emptyCtx) Deadline() (deadline time.Time, ok bool) { + return +} + +func (*emptyCtx) Done() <-chan struct{} { + return nil +} + +func (*emptyCtx) Err() error { + return nil +} + +func (*emptyCtx) Value(key interface{}) interface{} { + return nil +} +``` + +注意 emptyCtx 类型也实现了 fmt.Stringer 接口。这使得我们可以做一个 fmt.Println(ctx)。 + +```go +fmt.Println(reflect.TypeOf(ctx)) +// *context.emptyCtx +fmt.Println(ctx) +// context.Background +``` + + + +## 8 为你的函数/方法添加 Context + +当你的 root context 被创建后,我们可以把它传递给函数或方法。 + +但在这之前,我们必须在我们的函数中添加一个上下文参数。 + +```go +func foo1(ctx context.Context, a int) { + //... +} +``` + +在前面的列表中,你注意到两个在go项目中被广泛使用的Go习语。 + +1. context 是一个函数的第一个参数 +2. context 的参数被命名为 `ctx` + +## 9 衍生出来的 Context + +我们已经在上一节中创建了我们的根上下文。这个上下文是空的;它什么也不做。我们可以做的是,从我们的空上下文衍生出另一个子上下文: + +![Deriving Contexts[fig:Deriving-Contexts]](imgs/deriving_context.ad1f8dc6.png) + +​ Deriving Contexts[fig:Deriving-Contexts] + +为了推导出一个 context,你可以使用以下函数。 + +* WithCancel +* WithTimeout +* WithDeadline +* WithValue + +## 10 取消 + +函数`WithCancel`只接受一个名为`parent`的参数。这个参数代表我们要派生的上下文。我们将创建一个新的上下文,而父上下文将保持对这个新的子上下文的引用。 + +让我们看一下WithCancel函数的签名: + +```go +func WithCancel(parent Context) (ctx Context, cancel CancelFunc) +``` + +此函数返回下一个子上下文和一个 CancelFunc。 CancelFunc 是上下文包的自定义类型: + +```go +type CancelFunc func() +``` + +`CancelFunc` 是一个命名类型,它的底层类型是 `func()`。 这个函数“告诉操作放弃它的工作”(Golang 来源)。 调用 `WithCancel` 将为我们提供一种取消操作的方法。 以下是如何创建派生上下文: + +```go +ctx, cancel := context.WithCancel(context.Background()) +``` + +要取消操作,您需要调用 cancel : + +``` +cancel() +``` + +## 11 WithTimeout / WithDeadline (超时 / 截止时间) + +超时是归因于进程正常完成的最长时间。 对于需要可变时间执行的任何进程,我们可以添加超时,即允许等待的固定时间。 如果没有超时,我们的应用程序可以无限期地等待进程完成。 + +```go +ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) +``` + +截止日期是指定的时间点。 当你设置一个截止日期时,你指定一个进程不会超过它。 + +```go +deadline := time.Date(2021, 12, 12, 3, 30, 30, 30, time.UTC) +ctx, cancel := context.WithDeadline(context.Background(), deadline) +``` + +## 12 用法示例 + +### 12.1 Without context + +举个例子:我们将设计一个应用程序,它必须向 Web 服务器发出 HTTP 请求以获取数据,然后将其显示给用户。 我们将首先考虑没有 context (上下文)的应用程序,然后我们将为其添加上下文。 + +#### 12.1.1 Client + +```go +package main + +import ( + "log" + "net/http" +) + +func main() { + req, err := http.NewRequest("GET", "http://127.0.0.1:8989", nil) + if err != nil { + panic(err) + } + resp, err := http.DefaultClient.Do(req) + if err != nil { + panic(err) + } + log.Println("resp received", resp) +} +``` + +我们这里有一个简单的 http 客户端。 我们创建一个调用“http://127.0.0.1:8989”的 GET 请求。 如果我们无法创建请求,我们会让我们的程序恐慌。 然后我们使用默认的 HTTP 客户端 (`http.DefaultClient`) 将请求发送到服务器(使用方法 `Do`). + +然后将收到的响应打印给用户。 + +#### 12.1.2 Server + +我们已经设置了我们的客户端。我们现在必须建立我们的假服务器。 + +```go +package main + +import ( + "fmt" + "log" + "net/http" + "time" +) + +func main() { + http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { + log.Println("request received") + time.Sleep(time.Second * 3) + fmt.Fprintf(w, "Response") // 向客户端发送数据 + log.Println("response sent") + + }) + err := http.ListenAndServe("127.0.0.1:8989", nil) // 设置监听端口 + if err != nil { + panic(err) + } +} +``` + +代码很简单。我们首先用函数.`http.HandleFunc`来设置我们的http处理器。这个函数需要两个参数,路径和响应请求的函数。 + +我们用指令`time.Sleep(time.Second * 3)`等待3秒,然后我们写出响应。这个睡眠在这里是为了伪造服务器回答所需的时间。在这种情况下,响应就是简单的 `"Response"`。 + +然后我们启动我们的服务器来监听 `127.0.0.1:8989` (localhost, port 8989). + +#### 12.1.3 First test + +首先,我们启动服务器; 然后,我们启动客户端。 3 秒后,客户端收到响应。 + +```shell +$ go run server.go +2019/04/22 12:17:11 request received +2019/04/22 12:17:14 response sent + +$ go run client.go +2019/04/22 12:17:14 resp received &{200 OK 200 HTTP/1.1 1 1 map[Content-Length:[8] Content-Type:[text/plain; charset=utf-8] Date:[Mon, 22 Apr 2019 10:17:14 GMT]] 0xc000132180 8 [] false false map[] 0xc00011c000 } +``` + +正如你所看到的,我们的客户端必须处理3秒的延迟。让我们在服务器的代码中增加这一点;假设我们现在睡眠时间为1分钟。我们的客户端将等待1分钟;它将阻止我们的应用程序1分钟。 + +我们可以在这里注意到,我们的客户端应用程序注定要等待服务器,即使它需要无限长的时间。 这不是一个很好的设计。 用户不会乐于无限期地等待应用程序回答。 在我看来,与其让他无限期地等待,不如对用户说发生了错误。 + +### 12.2 客户端上下文 + +我们将保留我们之前创建的代码的基础。 我们将从创建 root context (根上下文) 开始: + +```go +rootCtx := context.Background() +``` + +然后,我们将把这个 context 衍生为一个新的上下文,称为ctx。 + +``` go +ctx, cancel := context.WithTimeout(rootCtx, 50*time.Millisecond) +``` + +* `WithTimeout` 函数有两个参数,一个是 context,一个是 time.Duration. +* 第二个参数是超时时间. +* 在这里,我们将其设置为 50 毫秒. +* 我建议您在实际应用程序中创建一个配置变量来保存超时持续时间。 通过这样做,您无需重新编译程序来更改超时. + +`context.WithTimeout`将返回: + +1. 衍生的 context +2. 一个取消函数 + +可以调用取消函数来警告子进程它应该放弃它正在做的事情。 调用取消将释放与上下文关联的资源。 为了确保在程序结束时调用取消函数,我们将使用 defer 语句: + +```go +defer cancel() +``` + +下一步包括创建请求并将我们的全新 context 附加到它: + +```go +req, err := http.NewRequest("GET", "http://127.0.0.1:8989", nil) +if err != nil { + panic(err) +} +// 为我们的请求添加context +req = req.WithContext(ctx) +``` + +其他地方与没有 context 的版本相同。 + +这是完整的客户端代码: + +```go +// context/client-side/main.go +package main + +import ( + "context" + "fmt" + "net/http" + "time" +) + +func main() { + rootCtx := context.Background() + req, err := http.NewRequest("GET", "http://127.0.0.1:8989", nil) + if err != nil { + panic(err) + } + // 创建 context + ctx, cancel := context.WithTimeout(rootCtx, 50*time.Millisecond) + defer cancel() + // 将 context 附加到我们的请求 + req = req.WithContext(ctx) + resp, err := http.DefaultClient.Do(req) + if err != nil { + panic(err) + } + fmt.Println("resp received", resp) +} +``` + +现在让我们测试我们的新客户端。 以下是服务器的日志: + +```tex +2019/04/24 00:52:08 request received +2019/04/24 00:52:11 response sent +``` + +我们看到我们收到了一个请求,并在 3 秒后发送了响应。 以下是我们客户的日志: + +```go +panic: Get http://127.0.0.1:8989: context deadline exceeded +``` + +我们看到 `http.DefaultClient.Do` 返回了一个 `error`。 + +* 文本说超过了最后期限。 +* 我们的请求已被取消,因为我们的服务器需要 3 秒来完成它的工作。 即使客户端已经取消了请求,服务器也会继续做这项工作。 我们必须找到一种在客户端和服务器之间共享该上下文的方法。 + +### 12.3 服务端上下文 + +#### 12.3.1 Headers + +HTTP 请求包括一组标头、一个正文和一个查询字符串。 当我们发送请求时,Go 不会传输任何有关请求上下文的信息。 + +如果要可视化请求的标头,可以在服务器代码中添加以下行: + +```go +fmt.Println("headers :") +for name, headers := range r.Header { + for _, h := range headers { + fmt.Printf("%s: %s\n", name, h) + } +} +``` + +我们使用循环遍历请求的标头并打印它们。 以下是与我们的客户端一起传输的标头: + +```tex +headers : +User-Agent: Go-http-client/1.1 +Accept-Encoding: gzip +``` + +我们只有两个标头。 第一个提供有关所用客户端的更多信息。 第二个通知服务器客户端可以接受压缩数据。 没有关于最终超时的事情。 + +但是如果我们看一下 `http.Requestobject`,我们可以注意到有一个名为 `Context()` 的方法。 此方法将检索请求的上下文。 如果尚未定义,它将返回一个空上下文: + +```go +func (r *Request) Context() context.Context { + if r.ctx != nil { + return r.ctx + } + return context.Background() +} +``` + +文档中说,"当客户端的连接关闭时,上下文被取消"。这意味着在go服务器的实现中,当客户端连接关闭时,取消函数被调用。 + +这意味着在我们的服务器内部,我们必须监听由ctx.Done()返回的通道。当我们在该通道上收到一个消息时,我们必须停止目前正在做的事情。 + +#### 12.3.2 doWork 函数 + +让我们看看如何将它引入我们的服务器. + +例如,我们将引入一个新函数 `doWork`。 它将代表我们的服务器处理的计算密集型任务。 这个 `doWork` 是 CPU 密集型操作的占位符。 + +![5](imgs/serverWithTimeout.78b3d9c5.png) + + 带有上下文活动图的 Http 服务器处理程序 + +我们将在一个单独的 goroutine 中启动 `doWork` 函数。 该函数将上下文和将写入其结果的通道作为参数。 我们来看看这个函数的代码: + +```go +// context/server-side/main.go +//... + +func doWork(ctx context.Context, resChan chan int) { + log.Println("[doWork] launch the doWork") + sum := 0 + for { + log.Println("[doWork] one iteration") + time.Sleep(time.Millisecond) + select { + case <-ctx.Done(): + log.Println("[doWork] ctx Done is received inside doWork") + return + default: + sum++ + if sum > 1000 { + log.Println("[doWork] sum has reached 1000") + resChan <- sum + return + } + } + } +} +``` + +在图 [5](#5) 中,您可以看到 doWork 函数的活动图。 + +在这个函数中,我们将使用一个通道与调用者进行通信。 我们创建了一个 for 循环,在该循环中,我们将放置一个 select 语句。 在这个 select 语句中,我们有两种情况: + +* `ctx.Done()` 返回的通道已关闭。 这意味着我们收到了完成工作的命令 + * 在这种情况下,我们将中断循环,记录一条消息并返回。 +* 默认情况(如果没有执行任何先前的情况,则执行) + * 在这种默认情况下,我们将递增总和. + * 如果变量总和严格来说大于1.000,我们将把结果发送到结果通道(``resChan'`)。 + +![Activity diagram of the doWork function[fig:doWork-fct]](imgs/doWorkFunction.15397b29.png) + +doWork函数的活动图 [fig:doWork-fct] + +#### 12.3.3 The server handler + +让我们看看我们将如何在我们的服务器处理程序中使用 `doWork` 函数: + +```go +// context/server-side/main.go +//... + +func main() { + http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { + log.Println("[Handler] request received") + // 检索请求的上下文 context + rCtx := r.Context() + // 创建结果通道 + resChan := make(chan int) + // 在goroutine中启动函数doWork + go doWork(rCtx, resChan) + // 等待 + // 1. 客户端断开连接 + // 2. 函数doWork完成它的工作 + select { + case <-rCtx.Done(): + log.Println("[Handler] context canceled in main handler, client has diconnected") + return + case result := <-resChan: + log.Println("[Handler] Received 1000") + log.Println("[Handler] Send response") + fmt.Fprintf(w, "Response %d", result) // 向客户端发送数据 + return + } + }) + err := http.ListenAndServe("127.0.0.1:8989", nil) // 设置听端口 + if err != nil { + panic(err) + } +} +``` + +我们已更改处理程序的代码以使用请求 Context。 这里要做的第一件事是检索请求的 Context: + +```go +rCtx := r.Context() +``` + +然后我们设置一个整数通道(`resChan`),它允许你与`doWork`函数进行通信。 我们将在单独的 goroutine 中启动 `doWork` 函数。 + +```go +resChan := make(chan int) +// 在goroutine中启动函数doWork +go doWork(rCtx, resChan) +``` + +然后,我们将使用select语句等待两个可能的事件: + +1. 客户端关闭连接;因此,取消通道将被关闭。 +2. 函数`doWork`已经完成了它的工作。(我们从 `resChan` 通道接收一个整数) + +在选项1中,我们记录一条消息,然后返回。当选项2发生时,我们使用来自 `resChan` 通道的结果,并将其写入响应写入器。我们的客户端将收到doWork函数计算的结果。 + +让我们运行服务器和客户机. 在图 [6](#fig:Execution-logs-client-server-context) 中 您可以看到客户机和服务器程序的执行日志。 + +您可以看到处理程序接收到请求,然后启动 `doWork` 函数。 然后处理程序接收取消信号。 然后将该信号传播到“doWork”函数。 + +![Execution logs for the client and the server[fig:Execution-logs-client-server-context]](imgs/server_client_timeout.4e32a6a4.png) + +Execution logs for the client and the server[fig:Execution-logs-client-server-context] + +## 13 截止日期 + +### 13.1 定义 + +`WithDeadline` 和 `WithTimeout` 非常相似。 如果我们查看 `context` 包的源代码,我们可以看到函数 `WithTimeout` 只是 `WithDeadline` 的一个包装器: + +```go +// source : context.go (in the standard library) +func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc) { + return WithDeadline(parent, time.Now().Add(timeout)) +} +``` + +如果您查看前面的代码片段,您可以看到超时持续时间已添加到当前时间。 让我们看看 WithDeadline 函数的签名: + +```go +func WithDeadline(parent Context, d time.Time) (Context, CancelFunc) + +``` + +这个函数有两个参数 : + +1. 一个父 Context +2. 一个特定的时间 + +### 13.2 用法 + +正如我们在上一节中所说,deadline 和 timeout 是相似的概念。 超时表示为持续时间,但最后期限表示为特定时间点(参见图 7) + +![Timeout vs Deadline[fig:Timeout-vs-Deadline]](imgs/timeout_deadline.b1d54bad.png) + +超时与截止日期[fig:Timeout-vs-Deadline] + +WithDeadline 可以用在你使用 WithTimeout 的地方。 这是标准库的示例: + +```go +// golang standard library +// src/net/dnsclient_unix.go +// line 133 + +// Exchange发送一个关于连接的查询并希望得到响应。 +func (r *Resolver) exchange(ctx context.Context, server string, q dnsmessage.Question, timeout time.Duration) (dnsmessage.Parser, dnsmessage.Header, error) { + //.... + for _, network := range []string{"udp", "tcp"} { + ctx, cancel := context.WithDeadline(ctx, time.Now().Add(timeout)) + defer cancel() + + c, err := r.dial(ctx, network, server) + if err != nil { + return dnsmessage.Parser{}, dnsmessage.Header{}, err + } + //... + } + return dnsmessage.Parser{}, dnsmessage.Header{}, errNoAnswerFromDNSServer +} + +``` + +* 这里的函数 `exchange` 将上下文作为第一个参数。 +* 对于每个网络(UDP 或 TCP),它衍生作为参数传递的Context。 +* 输入 Context 是通过调用 `context.WithDeadline` 衍生的。 截止时间是通过将超时持续时间添加到当前时间来创建的:`time.Now().Add(timeout)` +* 请注意,在创建衍生上下文后**immediately**,会延迟调用由`context.WithDeadline` 返回的取消函数。 这意味着当函数交换将返回时,取消函数将被调用。 +* 例如,如果拨号函数由于某种原因返回错误,交换函数将返回,取消函数将被调用,并且取消信号将被传播到子 Context。 + +## 14 取消传播 + +本节将深入探讨取消传播的机制。让我们举一个例子: + +```go +func main(){ + ctx1 := context.Background() + ctx2, c := context.WithCancel(ctx1) + defer c() +} +``` + +在这个小程序中,我们首先定义一个 root Context:`ctx1`。 然后我们通过调用 `context.WithCancel` 衍生这个Context。 + +Go将创建一个新的结构。 被调用的函数如下所示: + +```go +// src/context/context.go + +// newCancelCtx返回一个初始化的cancelCtx +func newCancelCtx(parent Context) cancelCtx { + return cancelCtx{Context: parent} +} +``` + +一个 `cancelCtx` 结构被创建,我们的root Context 被嵌入其中。 这里的类型 struct `cancelCtx` : + +```go +// src/context/context.go + +type cancelCtx struct { + Context + + mu sync.Mutex // 一把锁,保护以下字段 + done chan struct{} // 惰性创建,由第一次取消调用关闭 + children map[canceler]struct{} // 在第一次取消呼叫时设置为nil + err error // 在第一次取消呼叫时设置为非空 +} +``` + +我们有五个字段: + +* `Context`(父级)是一个嵌入的字段(它没有明确的字段名称) +* 互斥锁(名为 `mu`) +* 一个名为`done`的通道 +* 一个名为`children`的字段,它是一个地图。 键是 `canceller` 类型,值是 `struct{}` 类型 +* 还有一个名为 `err` 的错误 + +Canceller 是一个接口: + +```go +// 取消器是可以直接取消的 Context 类型 +// 实现是 *cancelCtx 和 *timerCtx。 +type canceler interface { + cancel(removeFromParent bool, err error) + Done() <-chan struct{} +} +``` + +实现接口取消器的类型必须实现到函数:cancel 函数和`done`函数。 + +![WithCancel create a derived context and a cancel function[fig:WithCancel-create-a]](imgs/withCancel.150ca94b.png) + +使用WithCancel创建一个衍生Context和一个取消函数 [fig:WithCancel-create-a] + +当我们执行取消函数时会发生什么? **ctx2** 会发生什么? + +* 互斥体 (`mu`) 将被锁定。 因此,没有其他 goroutine 能够修改这个上下文。 +* 通道 (`done`) 将被关闭 +* `ctx2` 的所有 children 也将被取消(在这种情况下,我们没有children ......) +* 互斥锁将被解锁。 + +### 14.0.0.1 二次推导 + +让我们扩展我们的示例并导出 ctx2: + +![Derive a derived context[fig:3-contexts]](imgs/withCancel3Derivatin.c23d2f44.png) + +获得一个衍生的 context[fig:3-contexts] + +```go +func main() { + ctx1 := context.Background() + ctx2, c2 := context.WithCancel(ctx1) + ctx3, c3 := context.WithCancel(ctx2) + //... +} +``` + +这里我们创建了`ctx3`**,**一个`cancelCtx`类型的新对象。 子 Context `ctx3` 将被添加到父 Context (`ctx2`)。 父 Context `ctx2` 将保留其子 Context 的内存。 目前,它只有一个子 `ctx3`(参见 [9](https://www.practical-go-lessons.com/chap-37-context#fig:3-contexts))。 + +现在我们来看看当我们调用取消函数c2时会发生什么。 + +* 互斥体 (`mu`) 将被锁定。 因此,没有其他 goroutine 能够修改这个上下文。 + +* 通道 (`done`) 将被关闭 + +* `ctx2` 的所有 children 也将被取消(在这种情况下,我们没有children ......) + + `ctx3` 将以相同的进程被取消 + +* 这里 `ctx1`(`ctx2` 的父级)是 `emptyCtx`,因此 `ctx2` 不会从 `ctx1` 中删除。 +* 互斥锁将被解锁。 + +### 14.0.0.2 三阶推导 + +现在让我们创建另一个衍生上下文。 + +```go +func main() { + ctx1 := context.Background() + ctx2, c2 := context.WithCancel(ctx1) + ctx3, c3 := context.WithCancel(ctx2) + ctx4, c4 := context.WithCancel(ctx3) +} +``` + +![3 derived contexts[fig:3-derived-contexts]](imgs/ctx_4_levels.f46f9ff9.png) + +3 derived contexts[fig:3-derived-contexts] + +* 正如您在图 [10](https://www.practical-go-lessons.com/chap-37-context#fig:3-derived-contexts) 中看到的,我们有一个 root Context 和三个后代。 +* 最后一个是`ctx4`。 +* 当我们调用 `c2` 时,它会取消 `ctx2` 以及它的子项(`ctx3`)。 +* 当 `ctx3` 将被取消时,它也将取消其所有子项,并且 `ctx4` 将被取消。 + +![Cancellation propagation[fig:Cancellation-propagation-1]](imgs/cancellation_propagation_2.51cb103e.png) + +取消传播[fig:Cancellation-propagation-1] + +本节的关键信息是“当您取消上下文时,取消操作将从父级传播到子级”。 + +## 15 一个重要的习惯用法:defer cancel() + +以下两行代码很常见: + +```go +ctx, cancel = context.WithCancel(ctx) +defer cancel() +``` + +您可以在标准库中遇到这些行,也可以在许多库中遇到这些行。 一旦我们衍生出现有的Context,就会在 defer 语句中调用取消函数。 + +正如我们之前所看到的,取消指令会从父代传播到子代;为什么我们需要明确地调用取消呢?当构建一个库时,你不确定有人会在父级 Context 中有效地执行取消函数。通过在延迟语句中加入对cancel的调用,你可以确保cancel会被调用: + +* 当函数返回(或到达其主体的末尾) + +* 或者当运行函数的 goroutine 发生恐慌时。 + +### 15.1 Goroutine 泄露 + +为了理解这种现象,我们举个例子。 + +首先,我们定义了两个函数:`doSth` 和 `doSth2`。 这两个功能是虚拟的。 他们将 Context 作为第一个参数。 然后他们无限期地等待 `ctx.Done()` close 返回的通道: + +```go +// context/goroutine-leak/main.go +// ... + +func doSth2(ctx context.Context) { + select { + case <-ctx.Done(): + log.Println("second goroutine return") + return + } +} + +func doSth(ctx context.Context) { + select { + case <-ctx.Done(): + log.Println("first goroutine return") + return + } +} +``` + +我们现在将在名为`launch`的第三个函数中使用这两个函数: + +```go +// context/goroutine-leak/main.go +// ... + +func launch() { + ctx := context.Background() + ctx, _ = context.WithCancel(ctx) + log.Println("launch first goroutine") + go doSth(ctx) + log.Println("launch second goroutine") + go doSth2(ctx) +} +``` + +在这个函数中,我们首先创建一个 root Context(由`context.Background`返回)。 然后我们推导出这个 root Context。 我们调用方法 `WithCancel()` 来获取可以取消的上下文。 + +然后我们启动我们的两个 goroutine。 现在让我们看看我们的主要功能: + +```go +// context/goroutine-leak/main.go +// ... + +func main() { + log.Println("begin program") + go launch() + time.Sleep(time.Millisecond) + log.Printf("Gouroutine count: %d\n", runtime.NumGoroutine()) + for { + } +} +``` + +我们在 goroutine 中启动函数 `launch`。 然后我们稍作停顿(1 毫秒),然后计算 goroutine 的数量。 在运行时包中定义了一个非常方便的函数: + +```go +runtime.NumGoroutine() +``` + +这里的goroutine的数量应该是3个:1个主goroutine + 1个执行doSth的goroutine + 1个执行`doSth2'的goroutine。如果我们不调用cancel,最后两个goroutine将无限期地运行。请注意,我们在程序中创建了另一个goroutine:启动`launch`**.**这个goroutine不会被计算在内,因为它几乎会瞬间返回。 + +当我们取消上下文时,我们的两个goroutines正在返回。因此,goroutines的数量将减少到1(main)。但在这里,我们根本没有调用取消函数。 + +下面是标准输出。 + +```shell +2019/05/04 19:01:16 begin program +2019/05/04 19:01:16 launch first goroutine +2019/05/04 19:01:16 launch second goroutine +2019/05/04 19:01:16 Gouroutine count: 3 +``` + +在主函数中,我们无法取消 Context(因为它是在启动函数中定义的)。 我们有 2 个泄露的 goroutine! 为了解决这个问题,我们可以修改函数启动并添加一个延迟语句: + +```go +// context/goroutine-leak-fixed/main.go +// ... + +func launch() { + ctx := context.Background() + ctx, cancel := context.WithCancel(ctx) + defer cancel() + log.Println("launch first goroutine") + go doSth(ctx) + log.Println("launch second goroutine") + go doSth2(ctx) +} +``` + +现在让我们看看通过运行我们的程序的这个修改版本获得的日志: + +```shell +2019/05/04 19:15:09 begin program +2019/05/04 19:15:09 launch first goroutine +2019/05/04 19:15:09 launch second goroutine +2019/05/04 19:15:09 first goroutine return +2019/05/04 19:15:09 second goroutine return +2019/05/04 19:15:09 Gouroutine count: 1 +``` + +在这里,我们杀死了两个泄露的 goroutine! + +## 16 WithValue + +context 可以**携带数据**。 此功能旨在与 **request-scoped** 数据一起使用,例如: + +* 凭据(例如 JSON Web 令牌) +* 请求 id(跟踪系统中的请求) +* 请求的 IP +* 一些标头(例如:用户代理) + +### 16.1 示例 + +```go +// context/with-value/main.go +package main + +import ( + "context" + "fmt" + "log" + "net/http" + + uuid "github.com/satori/go.uuid" +) + +func main() { + http.HandleFunc("/status", status) + err := http.ListenAndServe(":8091", nil) + if err != nil { + log.Fatal(err) + } +} + +type key int + +const ( + requestID key = iota + jwt +) + +func status(w http.ResponseWriter, req *http.Request) { + // 将请求 ID 添加到 context + ctx := context.WithValue(req.Context(), requestID, uuid.NewV4().String()) + // 将凭据添加到 context + ctx = context.WithValue(ctx, jwt, req.Header.Get("Authorization")) + + + upDB, err := isDatabaseUp(ctx) + if err != nil { + http.Error(w, "Internal server error", http.StatusInternalServerError) + return + } + upAuth, err := isMonitoringUp(ctx) + if err != nil { + http.Error(w, "Internal server error", http.StatusInternalServerError) + return + } + fmt.Fprintf(w, "DB up: %t | Monitoring up: %t\n", upDB, upAuth) +} + + +func isDatabaseUp(ctx context.Context) (bool, error) { + // 检索请求ID值 + reqID, ok := ctx.Value(requestID).(string) + if !ok { + return false, fmt.Errorf("requestID in context does not have the expected type") + } + log.Printf("req %s - checking db status", reqID) + return true, nil +} + +func isMonitoringUp(ctx context.Context) (bool, error) { + // 检索请求ID值 + reqID, ok := ctx.Value(requestID).(string) + if !ok { + return false, fmt.Errorf("requestID in context does not have the expected type") + } + log.Printf("req %s - checking monitoring status", reqID) + return true, nil +} +``` + +* 我们创建了一个正在监听 localhost:8091 的服务器 +* 此服务器有一个路由:`"/status"` +* 我们使用 `ctx := context.WithValue(req.Context(), requestID, uuid.NewV4().String())` 导出请求上下文 (`req.Context()`) + * 我们在 context 中添加一个键值对:requestID +* 然后我们更新操作。 我们添加一个新的密钥对来保存请求凭据:`ctx = context.WithValue(ctx, jwt, req.Header.Get("Authorization"))` + +然后,context 的值可以进入`isMonitoringUp`和`isMonitoringUp`。 + +```go +reqID, ok := ctx.Value(requestID).(string) +if !ok { + return false, fmt.Errorf("requestID in context does not have the expected type") +} +``` + +### 16.2 Key type (密匙类型) + +这是 WithValue 方法的标头: + +```go +func WithValue(parent Context, key, val interface{}) Context +``` + +参数 `key` 和 `val` 的类型是 `interface{}`。 换句话说,它们可以有任何类型。 只应遵守一个限制,`key` 的类型应该是 **comparable**。 + +* 我们可以跨多个包共享一个 context. +* 您可能希望限制对添加值的包之外的 context 值的访问. +* 为此,您可以创建一个未导出的类型 +* 所有键都属于这种类型. +* 我们将在包内全局定义键 : + +```go +type key int + +const ( + requestID key = iota + jwt +) +``` + +在前面的示例中,我们创建了一个底层类型为 int(可比较)的类型键。 然后我们定义了两个未导出的全局常量。 然后使用这些常量添加一个值并从上下文中检索一个值: + +```go +// 添加一个值 +ctx := context.WithValue(req.Context(), requestID, uuid.NewV4().String()) + +// 获取一个值 +reqID, ok := ctx.Value(requestID).(string) +``` + +#### 16.2.0.1 缺少的值和预期类型与实际类型不同 + +* 当在上下文中找不到键值对时,`ctx.Value` 将返回 `nil`。 +* 这就是我们制作**类型断言**的原因:保护我们免受缺失值或没有所需类型的值的影响。 + +### 16.3 问题 + +1. 截止日期和超时有什么区别? +2. 填空。 链表的每个节点都包含一个____值和一个____。 +3. 如何创建一个空的 context? +4. 您可以使用哪些方法导出 context? +5. 当 context 中没有找到键值对时,`ctx.Value(key)` 返回什么? + +### 16.4 答案 + +1. 截止日期和超时有什么区别? + 1. 截止日期是一个精确的时间点。 例如:2027年12月12日 + 2. 超时:持续时间。 例如:12 秒 +2. 填空。 链表的每个节点都包含一个____值和一个____。 + 1. 链表的每个节点都包含一个数据值和下一个元素在内存中的地址 + 2. 最终节点除外。 它没有设置地址。 +3. 如何创建一个空的 context? + 1. ctx := context.Background() +4. 您可以使用哪些方法导出 context? + 1. WithCancel + 2. WithTimeout + 3. WithDeadline + 4. WithValue +5. 当 context 中没有找到键值对时,`ctx.Value(key)` 返回什么? + 1. nil + +## 17 主要收获 + +* Context 是标准库中的一个包 + +* 我们可以使用 context 来: + + * 取消传播(例如:如果 API 使用者断开连接,则取消所有工作链) + * 与调用堆栈一起传输请求范围的数据。 + * 设置最后期限和超时。 + +* 截止日期=一个精确的时间点 + +* 超时 = 持续时间 + +* 在内部,包 context 是使用链表构建的。 + +* 链表是数据的集合。 链表的每个节点都包含一个数据值和下一个元素在内存中的地址。 + +* 要创建一个空 context,请使用: + + ```go + ctx := context.Background() + ``` + +* 我们可以使用以下方法导出每个 context: + * WithCancel : `ctx, cancel := context.WithCancel(context.Background())` + * WithTimeout : `ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)` + * WithDeadline : `ctx, cancel := context.WithDeadline(context.Background(), deadline)` + * WithValue : `ctx := context.WithValue(context.Background(),"key","value")` +* 通过衍生 context,您可以在上下文链接列表中创建一个节点. +* 当您取消上下文时,取消操作将从父上下文传播到子上下文。 +* context 可以携带请求范围的值。 +* 要**添加**一个值,请像这样导出上下文:`ctx := context.WithValue(req.Context(), requestID, uuid.NewV4().String())` +* 要从上下文中**检索**一个值,请使用以下语法:`reqID, ok := ctx.Value(requestID).(string)` +* 当没有使用给定键 `ctx.Value` 检索到值时,将返回 `nil` +* 通常,未导出类型的全局未导出变量/常量用作键。 + +## Bibliography + +* [abowd1999towards] Abowd, Gregory D, Anind K Dey, Peter J Brown, Nigel Davies, Mark Smith, and Pete Steggles. 1999. “Towards a Better Understanding of Context and Context-Awareness.” In International Symposium on Handheld and Ubiquitous Computing, 304–7. Springer. diff --git a/chapter-37-Context/imgs/cancellation_propagation.7f4e0613.png b/chapter-37-Context/imgs/cancellation_propagation.7f4e0613.png new file mode 100644 index 0000000000000000000000000000000000000000..ee609efcaeb5e44a7ffe35bcdd95ebb2eb48da88 Binary files /dev/null and b/chapter-37-Context/imgs/cancellation_propagation.7f4e0613.png differ diff --git a/chapter-37-Context/imgs/cancellation_propagation_2.51cb103e.png b/chapter-37-Context/imgs/cancellation_propagation_2.51cb103e.png new file mode 100644 index 0000000000000000000000000000000000000000..af67ee1ee55469a1f2caf5f16ec592252fe20be6 Binary files /dev/null and b/chapter-37-Context/imgs/cancellation_propagation_2.51cb103e.png differ diff --git a/chapter-37-Context/imgs/context.92f43817.jpg b/chapter-37-Context/imgs/context.92f43817.jpg new file mode 100644 index 0000000000000000000000000000000000000000..56b5bad1bd1e8b53a7c7bdaa45988624bc540c06 Binary files /dev/null and b/chapter-37-Context/imgs/context.92f43817.jpg differ diff --git a/chapter-37-Context/imgs/ctx_4_levels.f46f9ff9.png b/chapter-37-Context/imgs/ctx_4_levels.f46f9ff9.png new file mode 100644 index 0000000000000000000000000000000000000000..930dd665686f0001c71f9f9ad02b2d6732aa7642 Binary files /dev/null and b/chapter-37-Context/imgs/ctx_4_levels.f46f9ff9.png differ diff --git a/chapter-37-Context/imgs/deriving_context.ad1f8dc6.png b/chapter-37-Context/imgs/deriving_context.ad1f8dc6.png new file mode 100644 index 0000000000000000000000000000000000000000..b6c48eb7a7b8e3757221fcc1cd6033239ec7e635 Binary files /dev/null and b/chapter-37-Context/imgs/deriving_context.ad1f8dc6.png differ diff --git a/chapter-37-Context/imgs/doWorkFunction.15397b29.png b/chapter-37-Context/imgs/doWorkFunction.15397b29.png new file mode 100644 index 0000000000000000000000000000000000000000..64551ea8912063bcd81c76211b398660f79abc49 Binary files /dev/null and b/chapter-37-Context/imgs/doWorkFunction.15397b29.png differ diff --git a/chapter-37-Context/imgs/linked_list.2f868379.png b/chapter-37-Context/imgs/linked_list.2f868379.png new file mode 100644 index 0000000000000000000000000000000000000000..a8ce468c398202c500db775f98c28f27c81c2815 Binary files /dev/null and b/chapter-37-Context/imgs/linked_list.2f868379.png differ diff --git a/chapter-37-Context/imgs/linked_list_pointers.3614379a.png b/chapter-37-Context/imgs/linked_list_pointers.3614379a.png new file mode 100644 index 0000000000000000000000000000000000000000..50cb63bf3b579b35c994975db9b39c4b143385aa Binary files /dev/null and b/chapter-37-Context/imgs/linked_list_pointers.3614379a.png differ diff --git a/chapter-37-Context/imgs/request_context.77c1b04c.png b/chapter-37-Context/imgs/request_context.77c1b04c.png new file mode 100644 index 0000000000000000000000000000000000000000..521a014aad3136f569fa6b81edcd54e7d1c69048 Binary files /dev/null and b/chapter-37-Context/imgs/request_context.77c1b04c.png differ diff --git a/chapter-37-Context/imgs/serverWithTimeout.78b3d9c5.png b/chapter-37-Context/imgs/serverWithTimeout.78b3d9c5.png new file mode 100644 index 0000000000000000000000000000000000000000..084bb5c781a273124a39a3a388f737d38ce7023b Binary files /dev/null and b/chapter-37-Context/imgs/serverWithTimeout.78b3d9c5.png differ diff --git a/chapter-37-Context/imgs/server_client_timeout.4e32a6a4.png b/chapter-37-Context/imgs/server_client_timeout.4e32a6a4.png new file mode 100644 index 0000000000000000000000000000000000000000..f0405131ec5cfc2f2e9a67faa9840dfc76114c54 Binary files /dev/null and b/chapter-37-Context/imgs/server_client_timeout.4e32a6a4.png differ diff --git a/chapter-37-Context/imgs/timeout_deadline.b1d54bad.png b/chapter-37-Context/imgs/timeout_deadline.b1d54bad.png new file mode 100644 index 0000000000000000000000000000000000000000..25f178e5561556946ecd2e1f2bce8e72116bc5d0 Binary files /dev/null and b/chapter-37-Context/imgs/timeout_deadline.b1d54bad.png differ diff --git a/chapter-37-Context/imgs/withCancel.150ca94b.png b/chapter-37-Context/imgs/withCancel.150ca94b.png new file mode 100644 index 0000000000000000000000000000000000000000..d9678fbddb9d6a7af179381c3fe9b3fd9aac6e6d Binary files /dev/null and b/chapter-37-Context/imgs/withCancel.150ca94b.png differ diff --git a/chapter-37-Context/imgs/withCancel3Derivatin.c23d2f44.png b/chapter-37-Context/imgs/withCancel3Derivatin.c23d2f44.png new file mode 100644 index 0000000000000000000000000000000000000000..97047d13699f8ad2606f6c96f7d76e028f2a3dce Binary files /dev/null and b/chapter-37-Context/imgs/withCancel3Derivatin.c23d2f44.png differ