如何在Go中使用上下文
作为 Write for DOnations 计划的一部分,作者选择了 Diversity in Tech Fund 来接受捐赠。
介绍
在开发大型应用程序时,尤其是在服务器软件中,有时除了函数自身工作所需的信息之外,了解更多关于它正在执行的环境的信息是有帮助的。 例如,如果 Web 服务器函数正在处理特定客户端的 HTTP 请求,则该函数可能只需要知道客户端请求哪个 URL 来提供响应。 该函数可能只将该 URL 作为参数。 但是,在提供响应时总是会发生一些事情,例如客户端在收到响应之前断开连接。 如果提供响应的函数不知道客户端已断开连接,则服务器软件最终可能会花费比计算永远不会使用的响应所需的更多计算时间。
在这种情况下,了解请求的上下文,例如客户端的连接状态,允许服务器在客户端断开连接后停止处理请求。 这可以在繁忙的服务器上节省宝贵的计算资源,并释放它们来处理另一个客户端的请求。 这种类型的信息在函数执行需要时间的其他上下文中也很有帮助,例如进行数据库调用。 为了使此类信息无处不在,Go 在其标准库中包含了一个 context
包。
在本教程中,您将首先创建一个在函数中使用上下文的 Go 程序。 然后,您将更新该程序以在上下文中存储附加数据并从另一个函数中检索它。 最后,您将使用上下文的能力来表示它已完成以停止处理其他数据。
先决条件
- Go version 1.16 or greater installed. To set this up, follow the How To Install Go tutorial for your operating system.
- 对 goroutines 和 channels 的理解,你可以在教程 How to Run Multiple Functions Concurrently in Go 中找到。
- 熟悉在 Go 中使用日期和时间,您可以在教程 How to Use Dates and Times in Go 中找到。
- 体验
switch
语句,您可以在教程 How To Write Switch Statements in Go 中了解更多信息。
创建上下文
Go 中的许多函数使用 context
包来收集有关它们正在执行的环境的附加信息,并且通常会为它们也调用的函数提供该上下文。 通过使用 context
包中的 context.Context
接口并将其从函数传递到函数,程序可以从程序的开始函数传递该信息,例如 main
,所有程序中最深的函数调用的方法。 例如,http.Request 的 Context 函数将提供一个 context.Context
,其中包含有关发出请求的客户端的信息,如果客户端在此之前断开连接,它将结束请求完成。 通过将此 context.Context
值传递给一个函数,然后调用 sql.DB 的 QueryContext 函数,如果数据库查询仍然存在,它也将停止客户端断开连接时运行。
在本节中,您将创建一个具有接收上下文作为参数的函数的程序。 您还将使用使用 context.TODO
和 context.Background
函数创建的空上下文调用该函数。
要开始在程序中使用上下文,您需要有一个目录来保存程序。 许多开发人员将他们的项目保存在一个目录中以使它们井井有条。 在本教程中,您将使用一个名为 projects
的目录。
首先,创建 projects
目录并导航到它:
mkdir projects cd projects
接下来,为您的项目创建目录。 在这种情况下,使用目录 contexts
:
mkdir contexts cd contexts
在 contexts
目录中使用 nano
或您喜欢的编辑器打开 main.go
文件:
nano main.go
在 main.go
文件中,您将创建一个接受 context.Context
作为参数的 doSomething
函数。 然后,您将添加一个 main
函数,该函数创建一个上下文并使用该上下文调用 doSomething
。
将以下行添加到 main.go
:
项目/上下文/main.go
package main import ( "context" "fmt" ) func doSomething(ctx context.Context) { fmt.Println("Doing something!") } func main() { ctx := context.TODO() doSomething(ctx) }
在 main
函数中,您使用了 context.TODO
函数,这是创建空(或起始)上下文的两种方法之一。 当您不确定要使用哪个上下文时,可以将其用作占位符。
在此代码中,您添加的 doSomething
函数接受 context.Context
作为其唯一参数,即使它还没有对它做任何事情。 变量的名称是 ctx
,通常用于上下文值。 还建议将 context.Context
参数作为函数中的第一个参数,您会在 Go 标准库中看到它。 但这还不适用,因为它是 doSomething
的唯一参数。
要运行您的程序,请在 main.go
文件上使用 go run
命令:
go run main.go
输出将类似于以下内容:
OutputDoing something!
输出显示使用 fmt.Println
函数调用并打印了 Doing something!
函数。
现在,再次打开您的 main.go
文件并更新您的程序以使用将创建空上下文的第二个函数 context.Background
:
项目/上下文/main.go
... func main() { ctx := context.Background() doSomething(ctx) }
context.Background
函数像 context.TODO
一样创建一个空上下文,但它设计用于您打算启动已知上下文的地方。 从根本上说,这两个函数做同样的事情:它们返回一个可以用作 context.Context
的空上下文。 最大的不同是你如何向其他开发者表明你的意图。 如果您不确定要使用哪一个,context.Background
是一个不错的默认选项。
现在,使用 go run
命令再次运行您的程序:
go run main.go
输出将类似于以下内容:
OutputDoing something!
您的输出将是相同的,因为代码的功能没有改变,只有开发人员在阅读代码时会看到的内容。
在本节中,您使用 context.TODO
和 context.Background
函数创建了一个空上下文。 但是,如果保持这种状态,空上下文对您并不完全有用。 您可以将它们传递给其他函数使用,但如果您想在自己的代码中使用它们,那么到目前为止您所拥有的只是一个空上下文。 将更多信息添加到上下文中可以做的事情之一是添加可以在其他函数中从它们中检索的数据,您将在下一节中执行此操作。
在上下文中使用数据
在程序中使用 context.Context
的一个好处是能够访问存储在上下文中的数据。 通过将数据添加到上下文并在函数之间传递上下文,程序的每一层都可以添加有关正在发生的事情的附加信息。 例如,第一个函数可以将用户名添加到上下文中。 下一个函数可以将文件路径添加到用户尝试访问的内容。 最后,第三个函数可以从系统磁盘读取文件并记录它是否成功加载以及哪个用户尝试加载它。
要向上下文添加新值,请使用 context
包中的 context.WithValue
函数。 该函数接受三个参数:父 context.Context
、键和值。 父上下文是将值添加到的上下文,同时保留有关父上下文的所有其他信息。 然后使用该键从上下文中检索值。 键和值可以是任何数据类型,但本教程将使用 string
键和值。 然后 context.WithValue
将返回一个新的 context.Context
值,其中添加了该值。
一旦你有一个添加了值的 context.Context
,你可以使用 context.Context
的 Value
方法访问这些值。 为 Value
方法提供一个键将返回存储的值。
现在,再次打开您的 main.go
文件并更新它以使用 context.WithValue
将值添加到上下文中。 然后,更新 doSomething
函数以使用 fmt.Printf
将该值打印到输出:
项目/上下文/main.go
... func doSomething(ctx context.Context) { fmt.Printf("doSomething: myKey's value is %s\n", ctx.Value("myKey")) } func main() { ctx := context.Background() ctx = context.WithValue(ctx, "myKey", "myValue") doSomething(ctx) }
在此代码中,您将新上下文分配回用于保存父上下文的 ctx
变量。 如果您没有理由引用特定的父上下文,这是一种相对常见的模式。 如果您确实也需要访问父上下文,您可以将此值分配给一个新变量,您很快就会看到。
要查看程序的输出,请使用 go run
命令运行它:
go run main.go
输出将类似于以下内容:
OutputdoSomething: myKey's value is myValue
在输出中,您将看到从 main
函数存储在上下文中的值现在也可用于 doSomething
函数。 在服务器上运行的较大程序中,此值可能类似于程序开始运行的时间,或者程序正在运行的服务器。
使用上下文时,重要的是要知道存储在特定 context.Context
中的值是不可变的,这意味着它们无法更改。 当您调用 context.WithValue
时,您传入了父上下文,并且您还收到了一个上下文。 由于 context.WithValue
函数没有修改您提供的上下文,因此您收到了一个上下文。 相反,它将您的父上下文包装在另一个具有新值的上下文中。
要查看它是如何工作的,请更新您的 main.go
文件以添加一个新的 doAnother
函数,该函数接受 context.Context
并从上下文中打印出 myKey
值。 然后,更新 doSomething
以在上下文中设置自己的 myKey
值 (anotherValue
),并使用生成的 anotherCtx
上下文调用 doAnother
。 最后,让 doSomething
再次从其原始上下文中打印出 myKey
值:
项目/上下文/main.go
... func doSomething(ctx context.Context) { fmt.Printf("doSomething: myKey's value is %s\n", ctx.Value("myKey")) anotherCtx := context.WithValue(ctx, "myKey", "anotherValue") doAnother(anotherCtx) fmt.Printf("doSomething: myKey's value is %s\n", ctx.Value("myKey")) } func doAnother(ctx context.Context) { fmt.Printf("doAnother: myKey's value is %s\n", ctx.Value("myKey")) } ...
接下来,使用 go run
命令再次运行您的程序:
go run main.go
输出将类似于以下内容:
OutputdoSomething: myKey's value is myValue doAnother: myKey's value is anotherValue doSomething: myKey's value is myValue
这次在输出中,您将看到 doSomething
函数的两行和 doAnother
函数的一行。 在 main
函数中,将 myKey
设置为 myValue
的值并将其传递给 doSomething
函数。 您可以在输出中看到 myValue
进入了函数。
但是,下一行显示,当您在 doSomething
中使用 context.WithValue
将 myKey
设置为 anotherValue
并传递结果 anotherCtx
上下文时到 doAnother
,新值会覆盖初始值。
最后,在最后一行,当您再次从原始上下文中打印出 myKey
值时,您会看到该值仍然是 myValue
。 由于 context.WithValue
函数仅包装父上下文,因此父上下文仍然具有它最初所做的所有相同值。 当您在上下文中使用 Value
方法时,它将找到给定键的最外层包装值并返回该值。 在您的代码中,当您为 myKey
调用 anotherCtx.Value
时,它将返回 anotherValue
因为它是上下文的最外层包装值,有效地覆盖任何其他 [X173X ] 值被包装。 当您再次在 doSomething
中调用 ctx.Value
时,anotherCtx
不会包裹 ctx
,因此会返回原始的 myValue
值。
注意: 上下文可以是一个强大的工具,它可以保存所有的值,但是需要在存储在上下文中的数据和作为参数传递给函数的数据之间取得平衡。 将所有数据放在上下文中并在函数中使用该数据而不是参数似乎很诱人,但这可能会导致代码难以阅读和维护。 一个好的经验法则是,函数运行所需的任何数据都应作为参数传递。 例如,有时将诸如用户名之类的值保留在上下文值中以供以后记录信息时使用会很有用。 但是,如果用户名用于确定函数是否应显示某些特定信息,则您希望将其作为函数参数包含在内,即使它已经在上下文中可用。 这样,当您或其他人将来查看该功能时,更容易查看实际使用了哪些数据。
在本节中,您更新了程序以将值存储在上下文中,然后包装该上下文以覆盖该值。 然而,正如前面一个例子中提到的,这不是唯一有价值的工具上下文可以提供的。 它们也可用于在应该停止处理以避免不必要的资源使用时向程序的其他部分发出信号。
结束上下文
context.Context
提供的另一个强大工具是一种向使用它的任何函数发出信号通知上下文已经结束并且应该被认为是完整的方法。 通过向这些函数发出上下文已完成的信号,他们知道停止处理与他们可能仍在处理的上下文相关的任何工作。 使用上下文的这个特性可以让你的程序更有效率,因为不是完全完成每个功能,即使结果会被抛出,处理时间可以用于其他事情。 例如,如果网页请求到达您的 Go Web 服务器,用户可能最终会在页面加载完成之前点击“停止”按钮或关闭浏览器。 如果他们请求的页面需要运行一些数据库查询,即使数据不会被使用,服务器也可能会运行查询。 但是,如果您的函数使用 context.Context
,您的函数将知道上下文已完成,因为 Go 的 Web 服务器将取消它,并且它们可以跳过运行它们尚未运行的任何其他数据库查询。 这将释放 Web 服务器和数据库服务器的处理时间,以便它可以用于不同的请求。
在本节中,您将更新程序以告知上下文何时完成,并使用三种不同的方法来结束上下文。
确定上下文是否完成
无论上下文结束的原因是什么,确定上下文是否完成都会以相同的方式发生。 context.Context
类型提供了一个名为 Done
的方法,可以检查上下文是否已经结束。 此方法返回 一个通道 ,该通道在上下文完成时关闭,任何等待它关闭的函数都知道它们应该认为它们的执行上下文已完成,并且应该停止与上下文相关的任何处理。 Done
方法之所以有效,是因为它的通道从未写入任何值,并且当通道关闭时,该通道将开始为每次读取尝试返回 nil
值。 通过定期检查 Done
通道是否已关闭并在其间进行处理工作,您可以实现一个可以工作但也知道是否应该提前停止处理的函数。 结合此处理工作,定期检查 Done
通道和 select
语句更进一步,允许您同时向其他通道发送数据或从其他通道接收数据。
Go 中的 select 语句用于允许程序尝试同时读取或写入多个通道。 每个 select
语句只发生一个通道操作,但是当在循环中执行时,当一个通道操作可用时,程序可以执行多个通道操作。 select
语句是通过使用关键字 select
创建的,后跟用大括号 ({}
) 括起来的代码块,以及一个或多个 case
语句代码块内。 每个 case
语句可以是通道读取或写入操作,并且 select
语句将阻塞,直到可以执行 case
语句之一。 不过,假设您不希望 select
语句阻塞。 在这种情况下,您还可以添加一个 default
语句,如果其他 case
语句都无法执行,该语句将立即执行。 它的外观和工作方式类似于 switch
语句,但用于通道。
下面的代码示例显示了 select
语句如何潜在地用于从通道接收结果的长时间运行的函数中,但也会监视上下文的 Done
通道何时关闭:
ctx := context.Background() resultsCh := make(chan *WorkResult) for { select { case <- ctx.Done(): // The context is over, stop processing results return case result := <- resultsCh: // Process the results received } }
在此代码中,ctx
和 resultsCh
的值通常会作为参数传递给函数,其中 ctx
是函数正在执行的 context.Context
和 resultsCh
是来自其他地方的工作 goroutine 的结果的只读通道。 每次运行 select
语句时,Go 都会停止运行该函数并监视所有 case
语句。 当 case
语句之一可以执行时,无论是在 resultsCh
情况下从通道读取,在 Done
通道,执行 select
语句的那个分支。 但是,如果多个语句可以同时运行,则不能保证 case
语句执行的顺序。
对于此示例中的代码执行,for
循环将一直持续到 ctx.Done
通道关闭,因为唯一的 return
语句在 case
语句中. 即使 case <- ctx.Done
没有给任何变量赋值,但是当 ctx.Done
关闭时它仍然会被触发,因为即使忽略它,通道仍然有一个可以读取的值。 如果 ctx.Done
通道未关闭,则 select
语句将一直等到它关闭,或者如果 resultsCh
有一个可以读取的值。 如果可以读取 resultsCh
,则将执行 case
语句的代码块。 由于没有保证顺序,如果两者都可以读取,那么执行哪一个似乎是随机的。
如果示例的 select
语句有一个 default
分支,其中没有任何代码,它实际上不会改变代码的工作方式,它只会导致 select
语句结束for
循环将立即开始 select
语句的另一次迭代。 这导致 for
循环执行得非常快,因为它永远不会停止并等待从通道读取。 发生这种情况时,for
循环称为 busy loop,因为循环不是等待某事发生,而是忙于一遍又一遍地运行。 这会消耗大量 CPU,因为程序永远不会有机会停止运行以让其他代码执行。 但是,有时此功能很有用,例如,如果您想在执行另一个非通道操作之前检查通道是否已准备好执行某项操作。
由于示例代码中退出for
循环的唯一方法是关闭Done
返回的通道,而关闭Done
通道的唯一方法是结束上下文,您需要一种结束上下文的方法。 Go context
包根据您的目标提供了几种方法来执行此操作,最直接的选择是调用函数来“取消”上下文。
取消上下文
取消上下文是结束上下文最直接和可控的方式。 与使用 context.WithValue
在上下文中包含值类似,可以使用 context.WithCancel
函数将“取消”函数与上下文相关联。 该函数接收一个父上下文作为参数并返回一个新上下文以及一个可用于取消返回的上下文的函数。 此外,与 context.WithValue
类似,调用返回的取消函数只会取消返回的上下文以及将其用作父上下文的任何上下文。 这不会阻止父上下文被取消,它只是意味着调用你自己的取消函数不会这样做。
现在,打开您的 main.go
文件并更新您的程序以使用 context.WithCancel
和取消功能:
项目/上下文/main.go
package main import ( "context" "fmt" "time" ) func doSomething(ctx context.Context) { ctx, cancelCtx := context.WithCancel(ctx) printCh := make(chan int) go doAnother(ctx, printCh) for num := 1; num <= 3; num++ { printCh <- num } cancelCtx() time.Sleep(100 * time.Millisecond) fmt.Printf("doSomething: finished\n") } func doAnother(ctx context.Context, printCh <-chan int) { for { select { case <-ctx.Done(): if err := ctx.Err(); err != nil { fmt.Printf("doAnother err: %s\n", err) } fmt.Printf("doAnother: finished\n") return case num := <-printCh: fmt.Printf("doAnother: %d\n", num) } } } ...
首先,为 time
包添加导入并更改 doAnother
函数以接受新的数字通道以打印到屏幕上。 接下来,您在 for
循环中使用 select
语句从该通道以及上下文的 Done
方法中读取。 然后,在 doSomething
函数中,您创建一个可以取消的上下文以及一个将数字发送到的通道,并将 doAnother
作为 goroutine 作为参数运行。 最后,您向通道发送一些数字并取消上下文。
要查看此代码运行,请像以前一样使用 go run
命令:
go run main.go
输出将类似于以下内容:
OutputdoAnother: 1 doAnother: 2 doAnother: 3 doAnother err: context canceled doAnother: finished doSomething: finished
在这个更新的代码中,你的 doSomething
函数就像一个函数,将工作发送到一个或多个从工作通道读取的 goroutines。 在这种情况下,doAnother
是工人,打印数字是它正在做的工作。 一旦 doAnother
goroutine 启动,doSomething
开始发送要打印的数字。 在 doAnother
函数内部,select
语句正在等待 ctx.Done
通道关闭或在 printCh
通道上接收数字。 doSomething
函数在通道上发送三个数字,每个数字触发fmt.Printf
,然后调用cancelCtx
函数取消上下文。 doAnother
函数从通道中读取三个数字后,等待下一个通道操作。 由于接下来发生的是 doSomething
调用 cancelCtx
,因此调用了 ctx.Done
分支。
当调用ctx.Done
分支时,代码使用context.Context
提供的Err
函数来判断上下文如何结束。 由于您的程序使用 cancelCtx
函数来取消上下文,因此您在输出中看到的错误是 context canceled
。
注意: 如果你之前运行过 Go 程序并查看了日志输出,你可能在过去看到过 context canceled
错误。 使用 Go http 包时,当客户端在服务器处理完整响应之前断开与服务器的连接时,这是一个常见错误。
一旦 doSomething
函数取消了上下文,它使用 time.Sleep
函数等待一小段时间,以给 doAnother
时间来处理取消的上下文并完成运行。 之后,它退出该功能。 在许多情况下,time.Sleep
不是必需的,但它是必需的,因为示例代码执行得如此之快。 如果不包括 time.Sleep
,则程序可能会在您在屏幕上看到程序的其余输出之前结束。
context.WithCancel
函数和它返回的取消函数在您想准确控制上下文何时结束时最有用,但有时您不想要或不需要这种控制量。 context
包中可用于结束上下文的下一个函数是 context.WithDeadline
,它是第一个自动为您结束上下文的函数。
给上下文一个截止日期
将 context.WithDeadline
与上下文一起使用可以让您设置上下文需要完成的截止日期,并且在该截止日期过去时它将自动结束。 为上下文设置截止日期类似于为自己设置截止日期。 你告诉上下文它需要完成的时间,如果超过了这个时间,Go 会自动为你取消上下文。
要设置上下文的截止日期,请使用 context.WithDeadline
函数并为其提供父上下文和应取消上下文的 time.Time
值。 然后,您将收到一个新上下文和一个取消新上下文作为返回值的函数。 与 context.WithCancel
类似,当超过最后期限时,它只会影响新上下文以及将其用作父上下文的任何其他上下文。 也可以通过调用取消函数来手动取消上下文,就像调用 context.WithCancel
一样。
接下来,打开您的 main.go
文件并将其更新为使用 context.WithDeadline
而不是 context.WithCancel
:
项目/上下文/main.go
... func doSomething(ctx context.Context) { deadline := time.Now().Add(1500 * time.Millisecond) ctx, cancelCtx := context.WithDeadline(ctx, deadline) defer cancelCtx() printCh := make(chan int) go doAnother(ctx, printCh) for num := 1; num <= 3; num++ { select { case printCh <- num: time.Sleep(1 * time.Second) case <-ctx.Done(): break } } cancelCtx() time.Sleep(100 * time.Millisecond) fmt.Printf("doSomething: finished\n") } ...
您更新的代码现在使用 doSomething
中的 context.WithDeadline
在函数启动后 1500 毫秒(1.5 秒)使用 time.Now
函数自动取消上下文。 除了更新上下文完成的方式之外,还进行了一些其他更改。 由于代码现在可能通过直接调用 cancelCtx
或通过截止日期自动取消来结束,因此 doSomething
函数已更新为使用 select
语句发送数字这个频道。 这样,如果从 printCh
读取的任何内容(在本例中为 doAnother
)都不是从通道读取并且 ctx.Done
通道关闭,则 [X149X ] 函数也会注意到它并停止尝试发送数字。
您还会注意到 cancelCtx
被调用了两次,一次是通过新的 defer
语句,另一次是在之前的位置。 defer cancelCtx()
不一定是必需的,因为另一个调用将始终运行,但如果将来有任何 return
语句导致它被错过,保留它会很有用. 当从截止日期取消上下文时,仍需要调用取消函数以清理已使用的任何资源,因此这更多是一种安全措施。
现在,使用 go run
再次运行您的程序:
go run main.go
输出将类似于以下内容:
OutputdoAnother: 1 doAnother: 2 doAnother err: context deadline exceeded doAnother: finished doSomething: finished
这次在输出中,您将看到在打印所有三个数字之前,由于 context deadline exceeded
错误而取消了上下文。 由于截止时间设置为 doSomething
功能开始运行后 1.5 秒,并且 doSomething
设置为在发送每个数字后等待一秒,因此在打印第三个数字之前将超过截止时间。 一旦超过最后期限,doAnother
和 doSomething
函数都将完成运行,因为它们都在等待 ctx.Done
通道关闭。 您还可以调整添加到 time.Now
的时间量,以查看各种截止日期如何影响输出。 如果最后期限以 3 秒或超过 3 秒结束,您甚至可以看到错误变回 context canceled
错误,因为不再超过最后期限。
如果您以前使用过 Go 应用程序或查看过它们的日志,那么您可能对 context deadline exceeded
错误也很熟悉。 此错误在需要一些时间才能完成向客户端发送响应的 Web 服务器中很常见。 如果数据库查询或某些处理需要很长时间,则可能导致 Web 请求花费的时间超过服务器允许的时间。 一旦请求超过限制,请求的上下文将被取消,您会看到 context deadline exceeded
错误消息。
使用 context.WithDeadline
而不是 context.WithCancel
结束上下文允许您指定上下文需要结束的特定时间,而无需自己跟踪该时间。 如果您知道上下文应该结束的 time.Time
,那么 context.WithDeadline
可能是管理上下文端点的好选择。 其他时候,你不关心上下文结束的具体时间,你只知道你希望它在它开始后一分钟结束。 可以使用 context.WithDeadline
和其他 time
包函数和方法来做到这一点,但 Go 还提供了 context.WithTimeout
函数来简化工作。
给上下文一个时间限制
context.WithTimeout
函数几乎可以被认为是对 context.WithDeadline
更有帮助的函数。 使用 context.WithDeadline
您可以提供特定的 time.Time
以结束上下文,但是通过使用 context.WithTimeout
函数,您只需提供 time.Duration
值多久你希望上下文持续下去。 在许多情况下,这将是您要查找的内容,但如果您需要指定 time.Time
,则可以使用 context.WithDeadline
。 如果没有 context.WithTimeout
,您需要自行使用 time.Now()
和 time.Time
的 Add
方法来获取具体的结束时间。
最后一次,打开您的 main.go
文件并将其更新为使用 context.WithTimeout
而不是 context.WithDeadline
:
项目/上下文/main.go
... func doSomething(ctx context.Context) { ctx, cancelCtx := context.WithTimeout(ctx, 1500*time.Millisecond) defer cancelCtx() ... } ...
更新并保存文件后,使用 go run
运行它:
go run main.go
输出将类似于以下内容:
OutputdoAnother: 1 doAnother: 2 doAnother err: context deadline exceeded doAnother: finished doSomething: finished
这次运行程序时,您应该会看到与使用 context.WithDeadline
时相同的输出。 错误消息甚至相同,显示 context.WithTimeout
实际上只是为您执行 time.Now
数学运算的包装器。
在本节中,您更新了程序以使用三种不同的方式结束 context.Context
。 第一个,context.WithCancel
,允许你调用一个函数来取消上下文。 接下来,您使用 context.WithDeadline
和 time.Time
值在特定时间自动结束上下文。 最后,您使用 context.WithTimeout
和 time.Duration
值在经过一定时间后自动结束上下文。 使用这些功能,您将能够确保您的程序不会消耗超过计算机所需的资源。 了解它们导致上下文返回的错误也将使您自己和其他 Go 程序中的错误故障排除变得更加容易。
结论
在本教程中,您创建了一个程序来以各种方式与 Go 的 context
包进行交互。 首先,您创建了一个接受 context.Context
值作为参数的函数,并使用 context.TODO
和 context.Background
函数创建空上下文。 之后,您使用 context.WithValue
将值添加到新上下文并使用 Value
方法在其他函数中检索它们。 最后,您使用了上下文的 Done
方法来确定上下文何时完成运行。 当与函数 context.WithCancel
、context.WithDeadline
和 context.WithTimeout
配对时,您实现了自己的上下文管理,以限制使用这些上下文的代码应该运行多长时间。
如果您想通过更多示例了解有关上下文如何工作的更多信息,Go context 包文档包含更多信息。
本教程也是 DigitalOcean How to Code in Go 系列的一部分。 该系列涵盖了许多 Go 主题,从第一次安装 Go 到如何使用语言本身。