如何在 Go 中使用上下文

作者选择了 多样性在技术基金作为 写给捐款计划的一部分接受捐款。

介绍

在开发大型应用程序时,尤其是在服务器软件中,有时有助于函数了解其正在执行的环境,而不是函数自行工作所需的信息。例如,如果 Web 服务器函数正在处理特定客户端的 HTTP 请求,则函数可能只需要知道客户端正在请求提供响应的 URL。 该函数可能只是将该 URL 作为参数。 然而,在服务响应时,事情总是可能发生,例如客户端在接收响应之前断开连接。

在这种情况下,意识到请求的背景,例如客户端的连接状态,允许服务器在客户端断开连接后停止处理请求,从而在忙碌的服务器上保存宝贵的计算资源,并释放它们来处理另一个客户端的请求。

在本教程中,您将首先创建一个 Go 程序,该程序在函数中使用一个文本. 然后,您将更新该程序以在文本中存储额外的数据,并从另一个函数中获取它。

前提条件

创建一个背景

许多函数在Go中使用背景包来收集有关他们正在执行的环境的额外信息,并且通常会为他们也称之为函数提供这种背景。通过在背景包中使用背景接口并将其从函数传输到函数,程序可以将该信息从程序的起始函数(如等)传输到程序中最深层次的函数调用处。 例如,由一个 http.Request的 [‘Context.Context’]函数(https://pkg.go.dev/net/http#Request.Context)传输到函数中,将提供一个包含有关客户端提出请求的信息的‘context.Context’函数,如果客户端在请求完成之前断开连接,它将结束。 通过将这个‘context.Context’

在本节中,您将创建一个函数的程序,该函数将接收一个文本作为参数,您还将使用您创建的context.TODOcontext.Background函数的空文本调用该函数。

要开始使用程序中的背景,你需要有一个目录,以保持程序。许多开发人员将他们的项目保存在目录中,以保持它们的组织。

首先,创建项目目录并导航到它:

1mkdir projects
2cd projects

接下来,为您的项目创建目录,在这种情况下,使用目录背景:

1mkdir contexts
2cd contexts

背景目录中使用nano或您最喜欢的编辑器来打开main.go文件:

1nano main.go

main.go文件中,您将创建一个doSomething函数,该函数将接受一个context.Context作为参数,然后,您将添加一个main函数,该函数将创建一个文本并使用该文本调用doSomething

将下列行添加到 main.go:

 1[label projects/contexts/main.go]
 2package main
 3
 4import (
 5    "context"
 6    "fmt"
 7)
 8
 9func doSomething(ctx context.Context) {
10    fmt.Println("Doing something!")
11}
12
13func main() {
14    ctx := context.TODO()
15    doSomething(ctx)
16}

函数中,您使用了context.TODO函数,这是创建空(或开始)背景的两种方式之一。

在这个代码中,你添加的doSomething函数接受了context.Context作为其唯一参数,尽管它还没有完全做任何事情。 变量的名称是ctx,通常用于语境值。 建议将context.Context参数作为函数的第一参数,你会在Go标准库中看到它。

要运行您的程序,请在 main.go 文件中使用go run命令:

1go run main.go

结果将看起来像这样:

1[secondary_label Output]
2Doing something!

输出显示您的函数被调用并使用fmt.Println函数打印做某事!

现在,重新打开您的 main.go 文件,并更新您的程序以使用创建一个空格背景的第二个函数,context.Background:

1[label projects/contexts/main.go]
2...
3
4func main() {
5    ctx := context.Background()
6    doSomething(ctx)
7}

「context.Background」函数创建了一个空的背景,就像「context.TODO」一样,但它被设计用于在你打算启动已知的背景的地方。 基本上,这两个函数做同样的事情:它们返回一个空的背景,可以用作「context.Context」。

现在,使用go run命令再次运行您的程序:

1go run main.go

结果将看起来像这样:

1[secondary_label Output]
2Doing something!

您的输出将是相同的,因为代码的功能没有改变,只有开发人员在阅读代码时会看到的。

在本节中,您使用context.TODOcontext.Background函数创建了一个空的背景,但是,空的背景对于您来说并不完全有用,如果它保持这种方式,您可以将其传输给其他函数来使用,但如果您想在自己的代码中使用它们,您到目前为止所拥有的只是空的背景。

在文脈中使用數據

在程序中使用context.Context 的一个好处是能够访问文本内存储的数据. 通过将数据添加到文本中并将文本从函数传输到函数中,一个程序的每个层可以添加有关正在发生的事情的额外信息。 例如,第一个函数可以将用户名添加到文本中。 接下来的函数可以将文件路径添加到用户试图访问的内容中。

若要将新值添加到文本中,请使用context.WithValue函数在context包中。该函数接受三个参数:母体context.Context、密钥和值。母体文本是将值添加到文本的文本,同时保留所有有关母体文本的其他信息。该密钥则用于从文本中获取值。关键和值可以是任何数据类型,但本教程将使用字符串键和值。

一旦你有一個「context.Context」與添加值,你可以使用「context.Context」的「Value」方法存取這些值。

现在,重新打开您的 main.go 文件,并更新它以使用 context.WithValue 将值添加到文本中,然后更新 doSomething 函数以使用 fmt.Printf 将该值打印到输出中:

 1[label projects/contexts/main.go]
 2...
 3
 4func doSomething(ctx context.Context) {
 5    fmt.Printf("doSomething: myKey's value is %s\n", ctx.Value("myKey"))
 6}
 7
 8func main() {
 9    ctx := context.Background()
10
11    ctx = context.WithValue(ctx, "myKey", "myValue")
12
13    doSomething(ctx)
14}

在此代码中,您正在将新文本分配回ctx变量中,该变量用于保留母文本. 这是一个相对常见的模式,如果您没有理由参考特定母文本. 如果您也需要访问母文本,您可以将此值分配给新变量,正如您很快看到的那样。

要查看程序的输出,请使用go run命令运行它:

1go run main.go

结果将看起来像这样:

1[secondary_label Output]
2doSomething: myKey's value is myValue

在输出中,你会看到你在文本中存储的值,因为函数现在也可在DoSomething函数中使用。

使用语境时,重要的是要知道存储在特定context.Context中的值是不可变的,这意味着它们不能被更改。当您调用context.WithValue时,您进入了主语境,并且还收到了一种语境。

要查看此功能,请更新您的 main.go 文件以添加一个新的 doAnother 函数,该函数接受 context.Context 并从文本中打印出 myKey 值。 然后,更新 doSomething 以在文本中设置自己的 myKey 值(anotherValue)并调用 doAnother 与结果的 anotherCtx 文本。 最后,让 doSomething 从原始文本中再次打印出 myKey 值:

 1[label projects/contexts/main.go]
 2...
 3
 4func doSomething(ctx context.Context) {
 5    fmt.Printf("doSomething: myKey's value is %s\n", ctx.Value("myKey"))
 6
 7    anotherCtx := context.WithValue(ctx, "myKey", "anotherValue")
 8    doAnother(anotherCtx)
 9
10    fmt.Printf("doSomething: myKey's value is %s\n", ctx.Value("myKey"))
11}
12
13func doAnother(ctx context.Context) {
14    fmt.Printf("doAnother: myKey's value is %s\n", ctx.Value("myKey"))
15}
16
17...

接下来,使用go run命令再次运行您的程序:

1go run main.go

结果将看起来像这样:

1[secondary_label Output]
2doSomething: myKey's value is myValue
3doAnother: myKey's value is anotherValue
4doSomething: myKey's value is myValue

这次在输出中,你会看到从doSomething函数的两个行和从doAnother函数的一个行。在你的主要函数中,你将myKey设置为myValue的值,然后将其传递到doSomething函数中。

然而,下一行显示,当您在doSomething中使用context.WithValuemyKey设置为anotherValue,并将结果的anotherCtx文本传输到doAnother,新值将超过初始值。

最后,在最后一行中,你会看到当你从原来的背景中再次打印myKey值时,该值仍然是myValue。由于context.WithValue函数只包裹了母语文本,母语文本仍然具有原有的所有值。当你在文本上使用Value方法时,它会找到给定的密钥的外部包装值并返回该值。在你的代码中,当你为myKey呼叫anotherCtx.Value时,它会返回anotherValue,因为它是文本中最外部包装的值,有效地超过任何其他被包装的myKey值。当你在Do Something内部呼叫ct.xValue时,`另一个Ctx

<$>[注] 注: 背景可以是一个强大的工具,它可以持有所有值,但需要在数据被存储在背景中和数据作为参数传输到函数之间取得平衡。 将所有数据放在一个背景中,并在函数中使用这些数据而不是参数,但这可能导致难以读取和维护的代码。 一个很好的例子是,函数运行所需的任何数据都应该作为参数传输。 有时,例如,在后期登录信息时使用文档中的用户名等值时,可以有用。

在本节中,您更新了程序以便在文本中存储一个值,然后将文本包装到文本中,以覆盖该值. 如前面提到的例子中所述,但这不是唯一有价值的工具文本可以提供。

结束一个背景

另一种强大的工具背景'。 上下文是一种向任何使用上下文的函数表明上下文已经结束并应被视为完整的方式。 通过向这些功能发出上下文的信号,他们知道停止处理他们可能仍在处理的上下文有关的任何工作。 使用上下文的这一特性可以使您的程序更有效率,因为即使结果被抛出,处理时间也并非完全完成每个函数,而是可以用于其它东西. 例如,如果一个网页请求来到您 go web 服务器,用户在页面完成加载前可能最终会点击"停止"按钮或关闭他们的浏览器. 如果他们要求的页面需要运行几个数据库查询,即使数据不会被使用,服务器也可能运行查询. 然而, 如果您的功能是使用context 。 上下文`,你的功能将知道上下文的实现,因为 Go的网络服务器将取消它,他们可以跳过运行其他还没有运行的数据库查询. 这将腾出网络服务器和数据库服务器的处理时间,从而可以用于不同的请求.

在本节中,你会更新你的程序,告诉你什么时候完成一个背景,并使用三种不同的方法来结束一个背景。

确定是否有一个背景

无论背景数据的终结的原因如何,确定背景是否完成都是一样的。context.Context类型提供一种称为Done的方法,可以检查数据是否结束了。该方法返回(一个频道)(https://andsky.com/tech/tutorials/how-to-run-multiple-functions-concurrently-in-go#communicating-safely-between-goroutines-with-channels),当背景完成时会关闭,并且任何观察其关闭的函数都会知道他们应该考虑他们的执行背景已经完成,并且应该停止任何与背景相关的处理。Done方法工作,因为没有任何值被写入其频道,而当频道关闭时,该频道将开始返回每个读取尝试的值。通过定期检查Done频道是否关闭并在中间进行处理工作,您可以实施一个可以工作但也知道是否应该早点停止处理的函数。结合这种处理工作,根据频道的Done定期

Go 中的 select声明用于允许程序试图同时从多个频道读取或写入多个频道。 每一个选择声明中只发生一个频道操作,但在循环中执行时,该程序可以在一个可用时执行多个频道操作。 使用关键字 select 创建一个 select 声明,然后在曲折的框架中包含一个代码块({}),在代码块内包含一个或多个 case 声明。 每一个 case 声明可以作为一个频道读取或写作操作,而 select 声明将被封锁,直到其中一个 case 声明能够执行。 假设您不希望‘select` 声明被封锁,然而. 在这种情况下,

下面的代码示例表明,一个选择语句在一个长期运行的函数中可能会被使用,该函数从一个渠道中接收结果,但还会监视当一个背景的完成渠道关闭时:

 1ctx := context.Background()
 2resultsCh := make(chan *WorkResult)
 3
 4for {
 5    select {
 6    case <- ctx.Done():
 7    	// The context is over, stop processing results
 8    	return
 9    case result := <- resultsCh:
10    	// Process the results received
11    }
12}

在此代码中,ctxresultsCh的值通常会被转移到一个函数作为参数,而ctx是执行函数的context.Context,而resultsCh是来自其他地方的工人goroutine的仅读的结果渠道。每次运行选择陈述时,Go会停止运行该函数并观察所有案例陈述。当一个案例陈述可以执行时,无论是从一个渠道读到resultsCh,写到一个渠道,还是在完成渠道的情况下关闭一个渠道,选择陈述的分支就会执行。

对于本示例中的代码执行,for循环将持续到ctx.Done频道关闭为止,因为唯一的返回陈述在case陈述中。 即使case <- ctx.Done不会为任何变量分配值,但在ctx.Done关闭时仍会触发,因为频道仍然具有可以读取的值,即使被忽视。 如果ctx.Done频道没有关闭,则select陈述会等到它存在,或者如果resultsCh有一个可读的值,那么如果resultsCh可以读取,那么该case陈述的代码块将被执行。

如果该示例的选择声明有一个默认分支而没有任何代码,它实际上不会改变代码的工作方式,它只会导致选择声明立即结束,而循环会开始选择声明的另一个迭代。这导致循环执行非常快,因为它永远不会停止并等待从一个频道读取。当这种情况发生时,循环被称为忙于循环,因为而不是等待某些事情发生,循环是忙于运行一遍又一遍。

由于在示例代码中退出循环的唯一方法是关闭由完成返回的频道,而关闭完成频道的唯一方法是通过终止文本,您需要一种方法来终止文本。

取消背景

取消语境是终止语境的最简单、最易于控制的方式。类似于在context.WithValue的语境中包含一个值,可以将取消函数与使用context.WithCancel函数的语境相关联。这个函数将母语语语境作为参数接收,并返回一个新的语境,以及一个可以用来取消返回语境的函数。同样,与context.WithValue相似,呼叫返回的 annul 函数只会取消返回的语境和使用它作为母语语境界的任何语境。这并不妨碍母语语语境被取消,只意味着呼叫自己的 annul 函数不会这样做。

现在,打开你的main.go文件,并更新你的程序使用context.WithCancel和取消函数:

 1[label projects/contexts/main.go]
 2package main
 3
 4import (
 5    "context"
 6    "fmt"
 7    "time"
 8)
 9
10func doSomething(ctx context.Context) {
11    ctx, cancelCtx := context.WithCancel(ctx)
12    
13    printCh := make(chan int)
14    go doAnother(ctx, printCh)
15
16    for num := 1; num <= 3; num++ {
17    	printCh <- num
18    }
19
20    cancelCtx()
21
22    time.Sleep(100 * time.Millisecond)
23
24    fmt.Printf("doSomething: finished\n")
25}
26
27func doAnother(ctx context.Context, printCh <-chan int) {
28    for {
29    	select {
30    	case <-ctx.Done():
31    		if err := ctx.Err(); err != nil {
32    			fmt.Printf("doAnother err: %s\n", err)
33    		}
34    		fmt.Printf("doAnother: finished\n")
35    		return
36    	case num := <-printCh:
37    		fmt.Printf("doAnother: %d\n", num)
38    	}
39    }
40}
41
42...

首先,您添加了时间包的导入,并更改了DoAnother函数,以接受一个新频道的数字打印到屏幕上。接下来,您使用一个选择陈述在一个循环中读取从该频道以及文本的完成方法。然后,在DoSomething函数中,您创建了一个可以被取消的背景以及一个可以发送数字的频道,并运行DoAnother作为参数。

要查看此代码运行,请使用如前所示的go run命令:

1go run main.go

结果将看起来像这样:

1[secondary_label Output]
2doAnother: 1
3doAnother: 2
4doAnother: 3
5doAnother err: context canceled
6doAnother: finished
7doSomething: finished

在此更新后的代码中,你的doSomething函数就像一个函数,将工作发送给一个或多个(https://andsky.com/tech/tutorials/how-to-run-multiple-functions-concurrently-in-go)从工作频道中读取的 [goroutines] 函数一样。在这种情况下,doAnother是工人,打印数字是它正在做的工作。一旦doAnother goroutine 启动,doSomething开始发送数字来打印。在doAnother函数内部,select声明正在等待ctx.Done频道关闭或在printCh频道上接收一个号码。doSomething函数在频道上发送三个号码,触发每个号码的fmt.Printf,然后呼叫cancelCtx函数取消

ctx.Done字段被调用时,代码使用context.Context提供的Err函数来确定文本如何结束.由于您的程序正在使用cancelCtx函数来取消文本,所以您在输出中看到的错误是文本取消

<$>[注] 注: 如果您以前运行过Go程序并查看了日志输出,您可能在过去看到过背景取消错误。

一旦doSomething函数取消了文本,它会使用time.Sleep函数等待短时间,给doAnother时间处理取消文本并完成运行。之后,它会退出该函数。在许多情况下,不需要time.Sleep,但它是必要的,因为示例代码完成执行如此之快。

「context.WithCancel」函数和返回的「cancel」函数最有用,当您想要准确地控制一个语境结束时,但有时您不需要或不需要这种数量的控制。

给一个背景一个截止日期

使用context.WithDeadline与文本设置时,您可以为文本需要完成时设定截止日期,并且在截止日期结束时自动结束。

若要为文本设置截止日期,请使用context.WithDeadline函数,并为其提供原始文本和time.Time值,以便在文本应该被取消时设置截止日期。 您将收到一个新的文本和一个函数以取消新文本作为返回值。

接下来,打开你的main.go文件,并更新它以使用context.WithDeadline而不是context.WithCancel:

 1[label projects/contexts/main.go]
 2...
 3
 4func doSomething(ctx context.Context) {
 5    deadline := time.Now().Add(1500 * time.Millisecond)
 6    ctx, cancelCtx := context.WithDeadline(ctx, deadline)
 7    defer cancelCtx()
 8
 9    printCh := make(chan int)
10    go doAnother(ctx, printCh)
11
12    for num := 1; num <= 3; num++ {
13    	select {
14    	case printCh <- num:
15    		time.Sleep(1 * time.Second)
16    	case <-ctx.Done():
17    		break
18    	}
19    }
20
21    cancelCtx()
22
23    time.Sleep(100 * time.Millisecond)
24
25    fmt.Printf("doSomething: finished\n")
26}
27
28...

您的更新代码现在使用context.WithDeadlinedoSomething中自动取消背景1500毫秒(1.5 秒)后,该函数通过使用time.Now函数开始使用。 除了更新背景的完成方式外,还做了几项其他更改。 由于代码现在可以通过直接呼叫cancelCtx或通过截止日期自动取消,所以doSomething函数已被更新,以使用选择语句在频道上发送数字。

您还会注意到cancelCtx被调用两次,一次是通过新的推迟声明,另一次是在以前的位置。推迟取消Ctx()不一定是需要的,因为另一个调用将始终运行,但如果未来有任何退出声明会导致它被错过,则可以保持它,因此当一个背景从截止日期中取消时,还需要调用取消函数以清除已使用的任何资源,因此这更是一种安全措施。

现在,使用go run再次运行您的程序:

1go run main.go

结果将看起来像这样:

1[secondary_label Output]
2doAnother: 1
3doAnother: 2
4doAnother err: context deadline exceeded
5doAnother: finished
6doSomething: finished

此时,在输出中,你会看到背景被取消,因为在所有三个数字都被打印之前出现了背景截止日期超过错误。由于截止日期设置为doSomething函数启动后 1.5 秒,并且doSomething在发送每个号码后设置为等待 1 秒,所以在第三个号码被打印之前,截止日期将超过。一旦截止日期超过,两个DoAnotherDoSomething函数都结束运行,因为它们都在观看ctx.Done频道关闭。您还可以调整添加到time.Now的时间量,以查看不同截止日期如何影响输出。如果截止日期结束时达到 3 秒或更长,您甚至可以看到错误的转变回到`

如果您以前使用过 Go 应用程序或查看过其日志,您可能也知道过境截止日期错误。此错误很常见在需要一些时间的 Web 服务器上看到。如果数据库查询或某些处理需要很长时间,则可能会导致 Web 请求比服务器允许更长时间。

使用context.WithDeadline而不是context.WithCancel来终止一个语境,你可以指定某个特定时刻,而不需要自己跟踪那个时刻,如果你知道某个语境应该终止的time.Time,那么context.WithDeadline很可能是管理你的语境的终点的好候选人。在其他情况下,你不关心某个语境结束的特定时刻,而你只知道你想要结束一分钟后才开始。

给一个背景一个时间限制

context.WithDeadline函数几乎可以被认为是围绕context.WithTimeout一个有用的函数。使用context.WithDeadline函数,您可以为结束的背景提供一个特定的time.Time,但使用context.WithTimeout函数,您只需要提供一个time.Duration值,用于您希望持续多长时间。

最後一次,開啟你的「main.go」檔案,並更新它以使用「context.WithTimeout」而不是「context.WithDeadline」:

 1[label projects/contexts/main.go]
 2...
 3
 4func doSomething(ctx context.Context) {
 5    ctx, cancelCtx := context.WithTimeout(ctx, 1500*time.Millisecond)
 6    defer cancelCtx()
 7
 8    ...
 9}
10
11...

一旦您更新并保存了文件,请使用go run运行它:

1go run main.go

结果将看起来像这样:

1[secondary_label Output]
2doAnother: 1
3doAnother: 2
4doAnother err: context deadline exceeded
5doAnother: finished
6doSomething: finished

这次运行程序时,你应该看到与你使用context.WithDeadline相同的输出.错误消息甚至是相同的,显示你context.WithTimeout实际上只是一个包装程序来为你做time.Now数学。

在本节中,您更新了您的程序以使用三种不同的方式来终止context.Context。第一个,context.WithCancel,允许您调用一个函数以取消语境。接下来,您使用了context.WithDeadline具有time.Time值来自动终止语境。最后,您使用了context.WithTimeout和一个time.Duration值来自动终止语境,经过一定的时间之后。 使用这些函数,您将能够确保您的程序不会在您的计算机上消耗比所需的更多资源。 了解导致语境返回的错误也会使您自己的和其他程序中的错误更容易解决。

结论

在本教程中,您创建了一种程序以多种方式与 Go 的背景包进行交互。首先,您创建了一种函数,该函数接受一个背景值作为参数,并使用了背景背景函数来创建空的背景。随后,您使用背景添加值到新的背景,并使用价值方法来检索它们在其他函数中。最后,您使用了背景的完成方法来确定一个背景的运行时间。

如果您想了解更多有关背景如何运作的示例,则 Go context包文档包含其他信息。

本教程也是 DigitalOcean How to Code in Go系列的一部分,该系列涵盖了许多 Go 主题,从首次安装 Go 到如何使用语言本身。

Published At
Categories with 技术
Tagged with
comments powered by Disqus