★
目前看过除了《go语言程序设计》以外最好的教程:https://www.practical-go-lessons.com/
”
原文:https://www.practical-go-lessons.com/chap-37-context
你将在本章中学到什么?
-
什么是上下文? -
什么是链表? -
如何使用上下文包?
涵盖的技术概念
-
Context derivation -
Linked list -
Context key-value pair -
Cancellation -
Timeout -
Deadline
介绍
章节专门介绍上下文包。在本章的第一部分,我们将了解什么是“上下文”以及它的目的是什么。在第二部分中,我们将了解如何在实际程序中使用上下文包。
什么是面向上下文的编程?
定义
上下文一词源自拉丁语“contexo”,意为将某事物与一组其他事物结合、连接、链接。在这里,我们认为某事物的上下文是与其他事物的一组链接和联系。在日常语言中,我们通常使用以下表达方式来描述上下文:
-
断章取义 -
在某事的背景下
事物、行为、言语都有其所处的上下文,与其他事物有联系。如果我们断章取义,就会简化事物,可能会误解其真实含义。上下文是改善决策的重要信息来源。上下文的一部分包括但不限于以下内容:
-
地点 -
日期 -
历史 -
人们
为了更好地理解上下文对我们的重要性,以下是一些例子:
-
在一次会议上,某人说:“这个想法不行。”如果我们只看到这句话,就可能会认为这个想法完全不可行。但如果我们了解到这个想法是在一个讨论中提出的,而且有其他人提出了类似的想法,那么我们就能更好地理解这个人的意思。 -
在一个历史事件中,我们需要了解当时的政治、社会和文化背景,才能真正理解事件的意义和影响。
因此,了解上下文对我们做出正确决策非常重要。
上下文可以增加对事件的理解
想象一下,在散步时,您听到两个人之间的对话:
-
爱丽丝: 你看过上周的比赛吗? -
鲍勃: 是的! -
爱丽丝: 在此之后,我相信他们会赢得下一场比赛! -
鲍勃: 当然,我愿意赌一千他们谈论“比赛”。
一支球队上周赢得了一场比赛,下周有很大机会赢得另一场比赛。我们不知道它是哪支球队和哪种运动。对话的上下文可以帮助我们理解它。如果对话发生在纽约,我们可以猜测它与棒球或篮球有关,因为这些运动在那里非常受欢迎。如果这次谈话发生在巴黎,他们谈论足球的可能性非常高。我们在这里所做的是添加上下文来理解某些内容。在这里我们谈到了这个地方。我们还可以在对话的上下文中添加时间因素。如果我们知道事情发生的时间,我们将能够浏览本周的体育比赛结果以更好地了解。
Context改变行为
对事件背景的分析可以改变参与者的行为。在回答以下问题时,请考虑上下文:
-
当你在自己的国家和在另一个国家时,你的礼仪是否有所不同? -
在办公室和与家人交流时,你使用的语言水平是否相同? -
在面试时,你是否会穿得比平时更正式?
答案可能是“否”,这是因为我们的行为受到环境的影响。具体情况会影响我们的行动方式,环境也会影响我们的行为和反应。
计算机科学中的Context
通常,我们设计计算机程序来执行预定义的任务。我们实现的指定例程始终以相同的方式执行,不会根据使用它的用户而改变。即使环境改变,程序的行为也不会改变。而面向上下文的编程思想则是在程序中引入受上下文影响的变化。1999年,Abowd提出了一个有趣的上下文定义:“上下文是我们可以用来描述实体情况的任何信息。实体是被认为与用户和应用程序之间的交互相关的人、地点或物体,包括用户和应用程序本身。”隐式和显式信息是上下文的构建块。程序员应该考虑上下文来构建可以在运行时调整其行为的应用程序。那么,什么是聪明呢?“智力”一词源自拉丁语词根“intellego”,意思是辨别、解开、注意、认识。如果某个东西能够辨别和理解,那么它就是聪明的。将上下文引入应用程序并不会让它们变得智能,但它们往往会让它们了解它们的环境和用户。
“Context”包:历史和用例
历史
该软件包首先由 Google 开发人员内部开发。它已被引入Go标准库中。在此之前,它可以在 Go 子存储库中使用。
用例
context 包有两个主要用途:
取消传播
为了理解这种用法,我们以一家名为 FooBar 的虚构建筑公司为例。 巴黎市委托这家公司建造一个巨大的游泳池。巴黎市长在民众代表中捍卫了其想法,该项目已获得批准。公司开始开展该项目;项目经理已经订购了建造水池所需的所有原材料。四个月过去了,市长换了,项目也取消了! FooBar 的项目经理很生气;该公司不得不取消156份订单。他开始通过电话一一加入他们。其中一些还从其他建筑公司订购原材料。每个人都在遭受这种形势的快速演变。 现在我们假设项目经理没有取消分包商的订单。其他公司将生产所需的商品,但不会获得报酬。这是对资源的极大浪费。 正如您在下图 中可以看到的那样,项目的取消正在传播给所有间接参与的工人。市议会取消该项目;FooBar 公司也取消了对承包商的订单。 在建筑和其他人类活动中,我们总是有办法取消工作。我们可以使用 context 包将取消政策引入到我们的程序中。当向 Web 服务器发出请求时,如果客户端断开连接,我们可以取消所有工作链!
调用堆栈中的数据传输
当向 Web 服务器发出请求时,负责处理请求的 Web 服务器功能不会单独完成该工作。请求将通过一系列函数和方法,然后发送响应。单个请求可以生成对微服务架构中其他微服务的新请求!这个函数调用链就是“调用堆栈”。我们将在本节中了解为什么随调用堆栈一起传输数据会很有用。 我们将举另一个例子:为购物应用程序开发 Web 服务器。我们有一个与我们的应用程序交互的用户。
-
用户将使用其网络浏览器进入登录; -
页面填写其登录详细信息; -
Web 浏览器将向服务器发送身份验证请求,服务器将请求转发到身份验证服务; -
服务器将构建“我的帐户”页面(例如通过模板)并发送用户的响应。 -
如果用户请求“最后订单”页面,服务器将需要调用订单服务来检索它们。
-
我们可以将发送请求的设备类型保留在上下文中。 -
如果设备是手机,我们可以选择加载轻量级模板来提高用户体验。 -
订单服务还可以只加载最后五个订单,以减少页面的渲染时间。 -
我们可以将经过身份验证的用户的 ID 保留在上下文中。 -
我们还可以保留传入请求的 IP。 -
身份验证层可以使用它来阻止可疑活动(引入阻止列表、检测错误、多次登录尝试) -
另一个非常常见的用例是生成单个请求 ID。requestId 被传递到应用程序的每一层。有了 ID,负责维护的团队就能够在日志中跟踪请求。(分布式链路跟踪)
设置截止日期和超时
截止日期:任务应该完成的时间。 超时:是一个非常相似的概念。我们不考虑日历中的精确日期和时间,而是考虑允许的最长持续时间。 我们可以使用上下文来定义长时间运行的进程的时间限制。这是一个例子:
-
您开发了一个服务器,并且客户端指定了 1 秒的超时。 -
可以设置一个超时时间为1秒的上下文;在此1秒之后,客户端将断开连接。 -
在这种情况下,我们再次希望避免浪费资源。
接口
context 包公开了一个由四个方法组成的接口:
type Context interface {
Deadline() (deadline time.Time, ok bool)
Done() <-chan struct{}
Err() error
Value(key interface{}) interface{}
}
在下一节中,我们将了解如何使用该包
链表
context 包是用标准数据结构构建的:链表。为了充分理解上下文是如何工作的,我们首先需要理解链表。 链表是数据元素的集合。列表中存储的数据类型不受限制;它可以是整数、字符串、结构、浮点数……等。列表的每个元素都是一个节点。每个节点包含两件事:
-
数据值 -
列表中下一个元素在内存中的地址。换句话说,这是一个指向下一个值的指针。
您可以在图 3 中看到链表的直观表示。该列表是“链接的”,列表中的节点有一个子节点(列表中的下一个元素)和一个父节点(列表中的最后一个元素)。请注意,这是不对的;列表中的第一个节点没有父节点。它是根源、起源、列表的头部。还有另一个值得注意的例外,即最终节点没有任何子节点。
root上下文: Background
在大多数程序中,我们在程序的root创建一个context.Background()。例如,在将启动我们的应用程序的主函数中。要创建根上下文,您可以使用以下语法:
ctx := context.Background()
对Background()函数的调用将返回一个指向空上下文的指针。在内部,对Background()的调用将创建一个新的context.emptyCtx。此类型未公开: 此类型未公开:
type emptyCtx int
emptyCtx 的基础类型是 int。该类型实现了 Context 接口所需的四个方法:
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):
fmt.Println(reflect.TypeOf(ctx))
// *context.emptyCtx
fmt.Println(ctx)
// context.Background
为您的函数/方法添加上下文
创建根上下文后,我们可以将其传递给函数或方法。但在此之前,我们必须向函数添加一个上下文参数:
func foo1(ctx context.Context, a int) {
//...
}
在前面的清单中,您注意到 go 项目中广泛使用的两个 Go 习惯用法:
-
上下文是函数的第一个参数, -
上下文参数名为 ctx
.
派生上下文
我们在上一节中创建了根上下文。这个上下文是空的;它什么也不做。我们可以做的是从空上下文中派生另一个子上下文:要派生上下文,您可以使用以下函数:
-
WithCancel -
WithTimeout -
WithDeadline -
WithValue
WithCancel
WithCancel
函数只接受一个名为parent 的参数。这个论证代表了我们想要导出的上下文。我们将创建一个新上下文,父上下文将保留对此新子上下文的引用。让我们看一下 WithCancel 函数的签名:
func WithCancel(parent Context) (ctx Context, cancel CancelFunc)
该函数返回下一个子上下文和 CancelFunc。CancelFunc 是上下文包的自定义类型:
type CancelFunc func()
CancelFunc
是一个命名类型,其底层类型是 func()
。该函数“告诉操作放弃其工作”(Golang 来源)。调用 WithCancel 将为我们提供取消操作的方法。以下是创建派生上下文的方法:
ctx, cancel := context.WithCancel(context.Background())
要取消操作,您需要调用 cancel :
cancel()
WithTimeout / WithDeadline
-
超时: 是进程正常完成所需的最长时间。对于任何需要可变时间执行的进程,我们可以添加超时,即允许等待的固定时间。如果没有超时,我们的应用程序可以无限期地等待进程完成。
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
-
截止日期: 是一个指定的时间点。当您设置最后期限时,您指定进程不会超过该期限。
deadline := time.Date(2021, 12, 12, 3, 30, 30, 30, time.UTC)
ctx, cancel := context.WithDeadline(context.Background(), deadline)
举例
没有上下文的情况
举个例子:我们将设计一个应用程序,它必须向 Web 服务器发出 HTTP 请求以获取数据,然后将其显示给用户。我们将首先考虑没有上下文的应用程序,然后向其添加上下文。客户端
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)。 然后将收到的响应打印给用户。服务端
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) // 等待3s
fmt.Fprintf(w, "Response") // 发送数据到服务端
log.Println("response sent")
})
err := http.ListenAndServe("127.0.0.1:8989", nil) // set listen port
if err != nil {
panic(err)
}
}
代码很简单。我们首先使用 function.http.HandleFunc 设置 http 处理程序。该函数有两个参数:路径和响应请求的函数。 我们使用 time.Sleep(time.Second * 3) 指令等待 3 秒,然后写入响应。这个睡眠是为了伪造服务器应答所需的时间。在这种情况下,响应只是“响应”。 然后我们启动服务器来监听 127.0.0.1:8989(本地主机,端口 8989)。测试首先,我们启动服务器;然后,我们启动客户端。3秒后,客户端收到响应。
$ 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 <nil>}
正如您所看到的,我们的客户端必须处理 3 秒的延迟。让我们在服务器的增加条件:假设我们现在睡了 1 分钟。我们的客户将等待1分钟;它会阻止我们的应用程序 1 分钟。我们可以在这里注意到, 我们的客户端应用程序注定要等待服务器,即使它需要无限长的时间。这不是一个很好的设计。用户不会乐意无限期地等待应用程序应答。在我看来,最好告诉用户发生了问题,而不是让他无限期地等待。
客户端上下文
我们将保留之前创建的代码的基础。我们将从创建根上下文开始:
rootCtx := context.Background()
然后我们将这个上下文导出到一个名为 ctx 的新上下文中:
ctx, cancel := context.WithTimeout(rootCtx, 50*time.Millisecond)
-
函数 WithTimeout 接受两个参数:上下文和 time.Duration。 -
第二个参数是超时持续时间。 -
这里我们将其设置为 50 毫秒。 -
我建议您在实际应用程序中创建一个配置变量来保存超时持续时间。通过这样做,您无需重新编译程序来更改超时。
context.WithTimeout 将返回:
-
派生上下文 -
取消功能
取消函数来警告子进程应该放弃正在执行的操作。调用 cancel 将释放与上下文关联的资源。为了确保在程序结束时调用取消函数,我们将使用 defer 语句:
defer cancel()
下一步包括创建请求并向其附加我们全新的上下文:
req, err := http.NewRequest("GET", "http://127.0.0.1:8989", nil)
if err != nil {
panic(err)
}
// 增加上下文到我们的请求中
req = req.WithContext(ctx)
其他行与没有上下文的版本相同。 完整的客户端代码:
// 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)
}
// create context
ctx, cancel := context.WithTimeout(rootCtx, 50*time.Millisecond)
defer cancel()
// attach context to our request
req = req.WithContext(ctx)
resp, err := http.DefaultClient.Do(req)
if err != nil {
panic(err)
}
fmt.Println("resp received", resp)
}
现在让我们测试我们的新客户端。以下是服务器的日志:
2019/04/24 00:52:08 request received
2019/04/24 00:52:11 response sent
我们看到我们收到了一个请求,并在 3 秒后发送了响应。以下是我们客户的日志:
panic: Get http://127.0.0.1:8989: context deadline exceeded
我们看到 http.DefaultClient.Do 返回了一个错误。
-
文中说已超过最后期限。 -
我们的请求已被取消,因为我们的服务器花了 3 秒来完成其工作。即使客户端取消了请求,服务器仍会继续执行工作。我们必须找到一种在客户端和服务器之间共享上下文的方法。
服务端上下文
Header
HTTP 请求由一组标头、正文和查询字符串组成。当我们发送请求时,Go 不会传输任何有关请求上下文的信息。 如果想可视化请求的标头,您可以在服务器代码中添加以下行:
fmt.Println("headers :")
for name, headers := range r.Header {
for _, h := range headers {
fmt.Printf("%s: %sn", name, h)
}
}
我们用循环遍历请求的标头并打印它们。以下是与我们的客户端传输的标头:
headers :
User-Agent: Go-http-client/1.1
Accept-Encoding: gzip
我们只有两个标题。第一个提供了有关所使用的客户端的更多信息。第二个通知服务器客户端可以接受 gzip 压缩的数据。与最终超时无关。 但是如果我们看一下 http.Request 对象,我们可以注意到有一个名为 Context() 的方法。此方法将检索请求的上下文。如果尚未定义,它将返回一个空上下文:
func (r *Request) Context() context.Context {
if r.ctx != nil {
return r.ctx
}
return context.Background()
}
文档说“当客户端连接关闭时,上下文将被取消”。这意味着在 go 服务器实现内部,当客户端连接关闭时,会调用取消函数。这意味着在我们的服务器内部,我们必须监听 ctx.Done() 返回的通道。当我们在该频道上收到消息时,我们必须停止当前正在做的事情。
doWork function
让我们看看如何将其引入我们的服务器。 例如,我们将引入一个新函数 doWork。它将代表我们的服务器处理的计算密集型任务。此 doWork 是 CPU 密集型操作的占位符。我们将在单独的 goroutine 中启动 doWork 函数。该函数将上下文和写入结果的通道作为参数。我们看一下这个函数的代码:
// 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 中,您可以看到 doWork 函数的活动图。在此函数中,我们将使用通道与调用者进行通信。我们创建一个 for 循环,并在该循环内放置一条 select 语句。在这个 select 语句中,我们有两种情况:
-
ctx.Done() 返回的通道已关闭。这意味着我们收到完成工作的命令。 -
在这种情况下,我们将中断循环,记录一条消息并返回。 -
默认情况(如果之前的情况没有执行则执行) -
在这种默认情况下,我们将增加总和。 -
如果变量 sum 变得严格大于 1.000,我们将在结果通道 (resChan) 上发送结果。

服务端的处理
// context/server-side/main.go
//...
func main() {
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
log.Println("[Handler] request received")
// 取出请求中的上下文
rCtx := r.Context()
// 创建结果接受通道
resChan := make(chan int)
// 加载一个异步程序
go doWork(rCtx, resChan)
// Wait for
// 1. 客户端丢失链接.
// 2. 函数完成
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) // send data to client side
return
}
})
err := http.ListenAndServe("127.0.0.1:8989", nil) // set listen port
if err != nil {
panic(err)
}
}
我们更改了处理程序的代码以使用请求上下文。这里要做的第一件事是检索请求的上下文:
rCtx := r.Context()
然后我们设置一个整数通道 (resChan),它允许您与 doWork 函数进行通信。我们将在单独的 goroutine 中启动 doWork 函数。
resChan := make(chan int)
// launch the function doWork in a goroutine
go doWork(rCtx, resChan)
然后我们将使用 select 语句来等待两个可能的事件:
-
客户端关闭连接;因此,取消通道将被关闭。 -
函数 doWork 已经完成了它的工作。(我们从 resChan 通道收到一个整数)
在选项 1 中,我们记录一条消息,然后返回。当选项 2 发生时,我们使用 resChan 通道的结果,并将其写入响应写入器。我们的客户端将收到 doWork 函数计算的结果。 让我们运行我们的服务器和客户端。在图6中您可以看到客户端和服务器程序的执行日志。 可以看到处理程序收到请求,然后启动 doWork 函数。然后处理程序接收取消信号。然后该信号被传播到 doWork 函数。
WithDeadline
WithDeadline 和 WithTimeout 非常相似。如果我们查看 context 包的源代码,我们可以看到函数 WithTimeout 只是 WithDeadline 的包装:
// source : context.go (in the standard library)
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc) {
return WithDeadline(parent, time.Now().Add(timeout))
}
如果您查看前面的代码片段,您可以看到超时持续时间已添加到当前时间。让我们看看 WithDeadline 函数的签名:
func WithDeadline(parent Context, d time.Time) (Context, CancelFunc)
该函数有两个参数:
-
父上下文 -
特定时间。
用法
正如我们在上一节中所说,截止日期和超时是类似的概念。超时表示为持续时间,而截止日期表示为特定时间点WithDeadline 可以在使用 WithTimeout 的地方使用。这是标准库的示例:
// golang standard library
// src/net/dnsclient_unix.go
// line 133
// exchange sends a query on the connection and hopes for a response.
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.WithDeadline 派生的。截止时间是通过将超时持续时间添加到当前时间来创建的:time.Now().Add(timeout) -
请注意,创建派生上下文后,立即会延迟调用 context.WithDeadline 返回的取消函数。这意味着当exchange函数返回时,取消函数将被调用。 -
例如,如果 dial 函数由于某种原因返回错误,则exchange函数将返回,取消函数将被调用,并且取消信号将传播到子上下文。
取消传播
本节将深入探讨取消传播的机制。让我们举个例子:
func main(){
ctx1 := context.Background()
ctx2, c := context.WithCancel(ctx1)
defer c()
}
在这个小程序中,我们首先定义一个根上下文:ctx1。然后我们通过调用 context.WithCancel 来派生此上下文。Go 将创建一个新的结构。将被调用的函数如下:
// src/context/context.go
// newCancelCtx returns an initialized cancelCtx.
func newCancelCtx(parent Context) cancelCtx {
return cancelCtx{Context: parent}
}
创建了一个 cancelCtx 结构,并将我们的根上下文嵌入其中。这里是类型 struct cancelCtx :
// src/context/context.go
type cancelCtx struct {
Context
mu sync.Mutex // protects the following fields
done chan struct{} // created lazily, closed by the first cancel call
children map[canceler]struct{} // set to nil by the first cancel call
err error // set to non-nil by the first cancel call
}
我们有五个字段:
-
上下文(父级)是一个嵌入字段(它没有明确的字段名称) -
互斥锁(名为 mu) -
名为done的通道 -
名为children 的字段是一个map。键的类型为 canceller,值的类型为 struct{} -
还有一个名为 err 的错误
Canceller 是一个接口:
type canceler interface {
cancel(removeFromParent bool, err error)
Done() <-chan struct{}
}
实现接口取消器的类型必须实现函数:取消函数和完成函数。当我们执行取消函数时会发生什么?ctx2 会发生什么?
-
互斥量 (mu) 将被锁定。因此,没有其他 goroutine 能够修改这个上下文。 -
通道(完成)将被关闭 -
ctx2 的所有子级也将被取消(在这种情况下,我们没有子级……) -
互斥体将被解锁。
两次传播

func main() {
ctx1 := context.Background()
ctx2, c2 := context.WithCancel(ctx1)
ctx3, c3 := context.WithCancel(ctx2)
//...
}
这里我们创建了 ctx3,一个类型为 cancelCtx 的新对象。子上下文 ctx3 将添加到父上下文 (ctx2)。父上下文 ctx2 将保留其子上下文的记忆。 目前,它只有一个子 ctx3。现在让我们看看当我们调用取消函数 c2 时发生了什么。
-
互斥量 (mu) 将被锁定。因此,没有其他 goroutine 能够修改这个上下文。 -
通道(完成)将被关闭 -
ctx2 的所有子级也将被取消(在这种情况下,我们没有子级……)ctx3将通过相同的过程被取消 -
ctx1(ctx2 的父级)是一个空的 Ctx,因此 ctx2 不会从 ctx1 中删除。 -
互斥体将被解锁。
三次传播
func main() {
ctx1 := context.Background()
ctx2, c2 := context.WithCancel(ctx1)
ctx3, c3 := context.WithCancel(ctx2)
ctx4, c4 := context.WithCancel(ctx3)
}
 
-
正如您在图 中看到的,我们有一个根上下文和三个后代。 -
最后一个是ctx4当我们调用 c2 时,它将取消 ctx2 及其子项 (ctx3)。 -
当 ctx3 被取消时,它也将取消其所有子项,并且 ctx4 将被取消。
本节的关键信息是“当您取消上下文时,取消操作将从父级传播到子级”。
一个重要的习惯用法:defer cancel()
下面两行代码很常见:
ctx, cancel = context.WithCancel(ctx)
defer cancel()
您可能会在标准库中遇到这些行,但也在许多库中遇到这些行。一旦我们派生出现有上下文,就会在 defer 语句中调用 cancel 函数。 正如我们之前所看到的,取消指令从父母传播到孩子;为什么我们需要显式调用cancel?在构建库时,您不确定是否有人会在父上下文中有效地执行取消函数。通过在延迟语句中添加对取消的调用,您可以确保调用取消:
-
当函数返回时(或到达其主体的末尾) -
或者当运行该函数的 goroutine 发生恐慌时。
Goroutine泄露
为了理解这个现象,我们举一个例子。 首先,我们定义两个函数:doSth 和 doSth2。这两个函数是虚拟的。他们将上下文作为第一个参数。然后他们无限期地等待 ctx.Done() close 返回的通道:
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 的第三个函数中使用这两个函数:
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)
}
在此函数中,我们首先创建一个根上下文(由 context.Background 返回)。然后我们导出这个根上下文。我们调用 WithCancel() 方法来获取可以取消的上下文。 然后我们启动两个 goroutine。现在让我们看一下我们的主要功能:
// context/goroutine-leak/main.go
// ...
func main() {
log.Println("begin program")
go launch()
time.Sleep(time.Millisecond)
log.Printf("Gouroutine count: %dn", runtime.NumGoroutine())
for {
}
}
我们在 goroutine 中启动函数。然后我们暂停一下(1 毫秒),然后计算 goroutine 的数量。运行时包中定义了一个非常方便的函数:
runtime.NumGoroutine()
这里的goroutine数量应该是3:1个主goroutine + 1个执行doSth的goroutine + 1个执行doSth2的goroutine。如果我们不调用 cancel,最后两个 goroutine 将无限期地运行。请注意,我们在程序中创建了另一个 goroutine:启动 launch 的 goroutine。这个 goroutine 不会被计算在内,因为它几乎会立即返回。当我们取消上下文时,我们的两个 goroutine 将返回。因此,goroutine 的数量将减少到 1 个(主协程)。但在这里,我们根本不调用取消函数。
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
在主函数中,我们无法取消上下文(因为它是在启动函数中定义的)。我们有 2 个泄漏的 goroutine!为了解决这个问题,我们只需修改函数 launch 并添加一个延迟语句:
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)
}
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
WithValue
上下文可以携带数据。此功能旨在与请求范围的数据一起使用,例如:
-
凭证(例如 JSON Web 令牌) -
请求id(用于在系统中跟踪请求) -
请求的IP -
一些标头(例如:用户代理)
举例
// 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添加到ctx中
ctx := context.WithValue(req.Context(), requestID, uuid.NewV4().String())
// 将认证凭证添加到ctx中
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: %tn", upDB, upAuth)
}
func isDatabaseUp(ctx context.Context) (bool, error) {
// 取出requestId
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) {
// 取出requestId
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()) -
我们向上下文添加一个键值对:requestID -
然后我们更新操作。我们添加一个新的密钥对来保存请求凭据: ctx = context.WithValue(ctx, jwt, req.Header.Get(“Authorization”))
然后可以通过 isDatabaseUp 和 isMonitoringUp 访问上下文值:
reqID, ok := ctx.Value(requestID).(string)
if !ok {
return false, fmt.Errorf("requestID in context does not have the expected type")
}
★
译者注: 在链路跟踪过程中,比较好的日志打印方式就是将traceID 作为日志的常驻字段后,将日志的子对象存入到ctx中,后面需要使用日志打印时,从ctx中取出log实例进行日志打印。
”
Key type
func WithValue(parent Context, key, val interface{}) Context
参数 key 和 val 的类型为 interface{}。换句话说,它们可以具有任何类型。仅应遵守一项限制,即key类型应具有可比性。我
-
们可以在多个包之间共享上下文。 -
您可能希望限制对添加值的包外部的上下文值的访问。 -
为此,您可以创建一个未导出的类型 -
所有的key都是这种类型的。 -
我们将在包内全局定义键:
type key int
const (
requestID key = iota
jwt
)
在前面的示例中,我们创建了一个具有基础类型 int (可比较)的类型键。然后我们定义了两个未导出的全局常量。然后使用这些常量添加值并从上下文中检索值:
// add a value
ctx := context.WithValue(req.Context(), requestID, uuid.NewV4().String())
// get a value
reqID, ok := ctx.Value(requestID).(string)
预期缺失值和类型与实际类型不同
-
当在上下文中找不到键值对时,ctx.Value 将返回 nil。 -
这就是我们进行类型断言的原因:保护我们免受缺失值或不具有所需类型的值的影响。
原文始发于微信公众号(小唐云原生):【翻译】万字长文细说Context
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/247585.html