跳转到主要内容

标签(标签)

资源精选(342) Go开发(108) Go语言(103) Go(99) angular(82) LLM(75) 大语言模型(63) 人工智能(53) 前端开发(50) LangChain(43) golang(43) 机器学习(39) Go工程师(38) Go程序员(38) Go开发者(36) React(33) Go基础(29) Python(24) Vue(22) Web开发(20) Web技术(19) 精选资源(19) 深度学习(19) Java(18) ChatGTP(17) Cookie(16) android(16) 前端框架(13) JavaScript(13) Next.js(12) 安卓(11) 聊天机器人(10) typescript(10) 资料精选(10) NLP(10) 第三方Cookie(9) Redwoodjs(9) LLMOps(9) Go语言中级开发(9) 自然语言处理(9) PostgreSQL(9) 区块链(9) mlops(9) 安全(9) 全栈开发(8) ChatGPT(8) OpenAI(8) Linux(8) AI(8) GraphQL(8) iOS(8) 软件架构(7) Go语言高级开发(7) AWS(7) C++(7) 数据科学(7) whisper(6) Prisma(6) 隐私保护(6) RAG(6) JSON(6) DevOps(6) 数据可视化(6) wasm(6) 计算机视觉(6) 算法(6) Rust(6) 微服务(6) 隐私沙盒(5) FedCM(5) 语音识别(5) Angular开发(5) 快速应用开发(5) 提示工程(5) Agent(5) LLaMA(5) 低代码开发(5) Go测试(5) gorm(5) REST API(5) 推荐系统(5) WebAssembly(5) GameDev(5) CMS(5) CSS(5) machine-learning(5) 机器人(5) 游戏开发(5) Blockchain(5) Web安全(5) Kotlin(5) 低代码平台(5) 机器学习资源(5) Go资源(5) Nodejs(5) PHP(5) Swift(5) 智能体(4) devin(4) Blitz(4) javascript框架(4) Redwood(4) GDPR(4) 生成式人工智能(4) Angular16(4) Alpaca(4) 编程语言(4) SAML(4) JWT(4) JSON处理(4) Go并发(4) kafka(4) 移动开发(4) 移动应用(4) security(4) 隐私(4) spring-boot(4) 物联网(4) nextjs(4) 网络安全(4) API(4) Ruby(4) 信息安全(4) flutter(4) 专家智能体(3) Chrome(3) CHIPS(3) 3PC(3) SSE(3) 人工智能软件工程师(3) LLM Agent(3) Remix(3) Ubuntu(3) GPT4All(3) 软件开发(3) 问答系统(3) 开发工具(3) 最佳实践(3) RxJS(3) SSR(3) Node.js(3) Dolly(3) 移动应用开发(3) 低代码(3) IAM(3) Web框架(3) CORS(3) 基准测试(3) Go语言数据库开发(3) Oauth2(3) 并发(3) 主题(3) Theme(3) earth(3) nginx(3) 软件工程(3) azure(3) keycloak(3) 生产力工具(3) gpt3(3) 工作流(3) C(3) jupyter(3) 认证(3) prometheus(3) GAN(3) Spring(3) 逆向工程(3) 应用安全(3) Docker(3) Django(3) R(3) .NET(3) 大数据(3) Hacking(3) 渗透测试(3) C++资源(3) Mac(3) 微信小程序(3) Python资源(3) JHipster(3) 大型语言模型(2) 语言模型(2) 可穿戴设备(2) JDK(2) SQL(2) Apache(2) Hashicorp Vault(2) Spring Cloud Vault(2) Go语言Web开发(2) Go测试工程师(2) WebSocket(2) 容器化(2) AES(2) 加密(2) 输入验证(2) ORM(2) Fiber(2) Postgres(2) Gorilla Mux(2) Go数据库开发(2) 模块(2) 泛型(2) 指针(2) HTTP(2) PostgreSQL开发(2) Vault(2) K8s(2) Spring boot(2) R语言(2) 深度学习资源(2) 半监督学习(2) semi-supervised-learning(2) architecture(2) 普罗米修斯(2) 嵌入模型(2) productivity(2) 编码(2) Qt(2) 前端(2) Rust语言(2) NeRF(2) 神经辐射场(2) 元宇宙(2) CPP(2) 数据分析(2) spark(2) 流处理(2) Ionic(2) 人体姿势估计(2) human-pose-estimation(2) 视频处理(2) deep-learning(2) kotlin语言(2) kotlin开发(2) burp(2) Chatbot(2) npm(2) quantum(2) OCR(2) 游戏(2) game(2) 内容管理系统(2) MySQL(2) python-books(2) pentest(2) opengl(2) IDE(2) 漏洞赏金(2) Web(2) 知识图谱(2) PyTorch(2) 数据库(2) reverse-engineering(2) 数据工程(2) swift开发(2) rest(2) robotics(2) ios-animation(2) 知识蒸馏(2) 安卓开发(2) nestjs(2) solidity(2) 爬虫(2) 面试(2) 容器(2) C++精选(2) 人工智能资源(2) Machine Learning(2) 备忘单(2) 编程书籍(2) angular资源(2) 速查表(2) cheatsheets(2) SecOps(2) mlops资源(2) R资源(2) DDD(2) 架构设计模式(2) 量化(2) Hacking资源(2) 强化学习(2) flask(2) 设计(2) 性能(2) Sysadmin(2) 系统管理员(2) Java资源(2) 机器学习精选(2) android资源(2) android-UI(2) Mac资源(2) iOS资源(2) Vue资源(2) flutter资源(2) JavaScript精选(2) JavaScript资源(2) Rust开发(2) deeplearning(2) RAD(2)

介绍


在开发大型应用程序时,尤其是在服务器软件中,有时除了函数独立运行所需的信息之外,了解更多关于它正在执行的环境的信息是有帮助的。例如,如果 Web 服务器函数正在处理特定客户端的 HTTP 请求,则该函数可能只需要知道客户端请求哪个 URL 来提供响应。该函数可能只将该 URL 作为参数。但是,在提供响应时总是会发生一些事情,例如客户端在收到响应之前断开连接。如果提供响应的函数不知道客户端已断开连接,则服务器软件最终可能会花费比计算永远不会使用的响应所需的更多计算时间。

在这种情况下,了解请求的上下文,例如客户端的连接状态,允许服务器在客户端断开连接后停止处理请求。这可以在繁忙的服务器上节省宝贵的计算资源,并释放它们来处理另一个客户端的请求。这种类型的信息在函数执行需要时间的其他上下文中也很有帮助,例如进行数据库调用。为了使对此类信息的普遍访问成为可能,Go 在其标准库中包含了一个上下文包。

在本教程中,您将首先创建一个在函数中使用上下文的 Go 程序。然后,您将更新该程序以在上下文中存储附加数据并从另一个函数中检索它。最后,您将使用上下文的能力来表示它已完成以停止处理其他数据。

先决条件

 

  • 安装 1.16 或更高版本。要进行设置,请按照您的操作系统的如何安装 Go 教程进行操作。
  • 对 goroutine 和通道的理解,您可以在教程 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 的目录。

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

mkdir projects
cd projects


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

mkdir contexts
cd contexts


在 contexts 目录中使用 nano 或您喜欢的编辑器打开 main.go 文件:

nano main.go


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

将以下行添加到 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


输出将类似于以下内容:


Output

Doing something!


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

现在,再次打开您的 main.go 文件并更新您的程序以使用将创建一个空上下文 context.Background 的第二个函数:

...

func main() {
    ctx := context.Background()
    doSomething(ctx)
}


context.Background 函数像 context.TODO 一样创建一个空上下文,但它被设计用于您打算启动已知上下文的地方。从根本上说,这两个函数做同样的事情:它们返回一个可以用作 context.Context 的空上下文。最大的不同是你如何向其他开发者表​​明你的意图。如果您不确定要使用哪一个, context.Background 是一个很好的默认选项。

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

go run main.go


输出将类似于以下内容:

Output
Doing something!


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

在本节中,您使用 context.TODO 和 context.Background 函数创建了一个空上下文。但是,如果保持这种状态,空上下文对您并不完全有用。您可以将它们传递给其他函数使用,但如果您想在自己的代码中使用它们,那么到目前为止您所拥有的只是一个空上下文。将更多信息添加到上下文中可以做的事情之一是添加可以在其他函数中从上下文中检索的数据,您将在下一节中执行此操作。

在上下文中使用数据


在程序中使用 context.Context 的一个好处是能够访问存储在上下文中的数据。通过将数据添加到上下文并在函数之间传递上下文,程序的每一层都可以添加有关正在发生的事情的附加信息。例如,第一个函数可以将用户名添加到上下文中。下一个函数可以将文件路径添加到用户尝试访问的内容。最后,第三个函数可以从系统磁盘读取文件并记录它是否成功加载以及哪个用户尝试加载它。

要将新值添加到上下文,请使用 context 包中的 context.WithValue 函数。该函数接受三个参数:父 context.Context、键和值。父上下文是将值添加到的上下文,同时保留有关父上下文的所有其他信息。然后使用该键从上下文中检索值。键和值可以是任何数据类型,但本教程将使用字符串键和值。然后 context.WithValue 将返回一个新的 context.Context 值,其中添加了该值。

一旦你有了一个添加了值的 context.Context,你就可以使用 context.Context 的 Value 方法来访问这些值。为 Value 方法提供一个键将返回存储的值。

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

...

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


输出将类似于以下内容:

Output

doSomething: myKey's value is myValue


在输出中,您将看到从 main 函数存储在上下文中的值现在也可以在 doSomething 函数中使用。在服务器上运行的较大程序中,此值可能类似于程序开始运行的时间,或者程序正在运行的服务器。

使用上下文时,重要的是要知道存储在特定上下文中的值。上下文是不可变的,这意味着它们不能更改。当您调用 context.WithValue 时,您传入了父上下文,并且您还收到了一个上下文。你收到了一个上下文,因为 context.WithValue 函数没有修改你提供的上下文。相反,它将您的父上下文包装在另一个具有新值的上下文中。

要查看它是如何工作的,请更新您的 main.go 文件以添加一个新的 doAnother 函数,该函数接受 context.Context 并从上下文中打印出 myKey 值。然后,更新 doSomething 以在上下文中设置自己的 myKey 值 (anotherValue),并使用生成的 anotherCtx 上下文调用 doAnother。最后,让 doSomething 再次从原始上下文中打印出 myKey 值:

...

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


输出将类似于以下内容:

  • Output
  • doSomething: myKey's value is myValue
  • doAnother: myKey's value is anotherValue
  • doSomething: myKey's value is myValue


这次在输出中,您将看到 doSomething 函数中的两行和 doAnother 函数中的一行。在您的主函数中,您将 myKey 设置为 myValue 的值并将其传递给 doSomething 函数。您可以在输出中看到 myValue 进入函数。

但是,下一行显示,当您在 doSomething 中使用 context.WithValue 将 myKey 设置为 anotherValue 并将生成的 anotherCtx 上下文传递给 doAnother 时,新值会覆盖初始值。

最后,在最后一行,您会看到当您再次从原始上下文中打印出 myKey 值时,该值仍然是 myValue。由于 context.WithValue 函数只包装了父上下文,因此父上下文仍然具有它最初所做的所有相同值。当您在上下文中使用 Value 方法时,它将找到给定键的最外层包装值并返回该值。在您的代码中,当您为 myKey 调用 anotherCtx.Value 时,它​​将返回 anotherValue,因为它是上下文的最外层包装值,有效地覆盖了任何其他被包装的 myKey 值。当你第二次在 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 语句阻塞。在这种情况下,您还可以添加一个默认语句,如果其他 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 函数正在执行,resultCh 是来自其他地方的工作 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 语句有一个默认分支,其中没有任何代码,它实际上不会改变代码的工作方式,它只会导致 select 语句立即结束,for 循环将开始 select 语句的另一个迭代。这导致 for 循环执行得非常快,因为它永远不会停止并等待从通道中读取。发生这种情况时,for 循环称为忙循环,因为循环不是等待某事发生,而是忙于一遍又一遍地运行。这会消耗大量 CPU,因为程序永远不会有机会停止运行以让其他代码执行。但是,有时此功能很有用,例如,如果您想在执行另一个非通道操作之前检查通道是否已准备好执行某项操作。

由于在示例代码中退出 for 循环的唯一方法是关闭 Done 返回的通道,而关闭 Done 通道的唯一方法是结束上下文,因此您需要一种结束上下文的方法。 Go 上下文包根据您的目标提供了几种方法来执行此操作,最直接的选择是调用一个函数来“取消”上下文。

取消上下文


取消上下文是结束上下文最直接和可控的方式。与使用 context.WithValue 在上下文中包含值类似,可以使用 context.WithCancel 函数将“取消”函数与上下文相关联。该函数接收一个父上下文作为参数并返回一个新上下文以及一个可用于取消返回的上下文的函数。此外,与 context.WithValue 类似,调用返回的取消函数只会取消返回的上下文以及任何使用它作为父上下文的上下文。这不会阻止父上下文被取消,它只是意味着调用你自己的取消函数不会这样做。

现在,打开您的 main.go 文件并更新您的程序以使用 context.WithCancel 和取消功能:

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

...

首先,为时间包添加一个导入并更改 doAnother 函数以接受新的数字通道以打印到屏幕上。接下来,您在 for 循环中使用 select 语句从该通道以及上下文的 Done 方法中读取。然后,在 doSomething 函数中,您创建一个可以取消的上下文以及一个将数字发送到的通道,并将 doAnother 作为 goroutine 运行,并将这些作为参数。最后,您向通道发送一些数字并取消上下文。

要查看此代码正在运行,请像以前一样使用 go run 命令:

go run main.go


输出将类似于以下内容:

Output
doAnother: 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 函数取消上下文,因此您在输出中看到的错误是上下文取消。

注意:如果你之前运行过 Go 程序并查看了日志输出,你可能在过去看到过 context cancelled 错误。使用 Go http 包时,当客户端在服务器处理完整响应之前断开与服务器的连接时,这是一个常见错误。

一旦 doSomething 函数取消了上下文,它使用 time.Sleep 函数等待一小段时间,以便给 doAnother 时间来处理取消的上下文并完成运行。之后,它退出该功能。在许多情况下, time.Sleep 不是必需的,但它是必需的,因为示例代码执行得如此之快。如果 time.Sleep 不包括在内,则程序可能会在您在屏幕上看到程序的其余输出之前结束。

context.WithCancel 函数和它返回的 cancel 函数在您想要准确控制上下文何时结束时最有用,但有时您不想要或不需要这种控制量。上下文包中可用于结束上下文的下一个函数是 context.WithDeadline ,它是第一个自动为您结束上下文的函数。

给上下文一个截止日期


将 context.WithDeadline 与上下文一起使用可以让您为上下文需要完成的时间设置截止日期,并且当该截止日期过去时它会自动结束。为上下文设置截止日期类似于为自己设置截止日期。你告诉上下文它需要完成的时间,如果超过了这个时间,Go 会自动为你取消上下文。

要为上下文设置截止日期,请使用 context.WithDeadline 函数并为其提供父上下文和 time.Time 值,以指示何时取消上下文。然后,您将收到一个新上下文和一个取消新上下文作为返回值的函数。与 context.WithCancel 类似,当超过最后期限时,它只会影响新上下文以及将其用作父上下文的任何其他上下文。也可以通过调用取消函数来手动取消上下文,就像调用 context.WithCancel 一样。

接下来,打开您的 main.go 文件并将其更新为使用 context.WithDeadline 而不是 context.WithCancel:

...

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 通道关闭,doSomething 函数也会注意到它并停止尝试发送数字。

您还会注意到 cancelCtx 被调用了两次,一次是通过新的 defer 语句,另一次是在之前的位置。 defer cancelCtx() 不一定是必需的,因为另一个调用将始终运行,但保留它可能很有用,以防将来有任何 return 语句导致它被错过。当从截止日期取消上下文时,仍需要调用取消函数以清理已使用的任何资源,因此这更多是一种安全措施。

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

go run main.go


输出将类似于以下内容:

Output
doAnother: 1
doAnother: 2
doAnother err: context deadline exceeded
doAnother: finished
doSomething: finished


这次在输出中,您将看到在打印所有三个数字之前,由于超出了上下文截止日期错误,上下文已被取消。由于在 doSomething 函数开始运行后将截止时间设置为 1.5 秒,并且将 doSomething 设置为在发送每个数字后等待一秒,因此在打印第三个数字之前将超过截止时间。一旦超过最后期限,doAnother 和 doSomething 函数都会完成运行,因为它们都在等待 ctx.Done 通道关闭。您还可以调整添加到时间的时间量。现在看看各种截止日期如何影响输出。如果最后期限结束于 3 秒或超过 3 秒,您甚至可以看到错误更改回上下文取消错误,因为不再超过最后期限。

如果您过去使用过 Go 应用程序或查看过它们的日志,您可能也熟悉超出上下文期限错误。此错误在需要一些时间才能完成向客户端发送响应的 Web 服务器中很常见。如果数据库查询或某些处理需要很长时间,则可能导致 Web 请求花费的时间超过服务器允许的时间。一旦请求超过限制,请求的上下文将被取消,您会看到超出上下文截止日期的错误消息。

使用 context.WithDeadline 而不是 context.WithCancel 结束上下文允许您指定上下文需要结束的特定时间,而无需自己跟踪该时间。如果您知道上下文应该结束的 time.Time,那么 context.WithDeadline 可能是管理上下文端点的好选择。其他时候,你不关心上下文结束的具体时间,你只知道你希望它在它开始后一分钟结束。可以使用 context.WithDeadline 和其他时间包函数和方法来做到这一点,但 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:

...

func doSomething(ctx context.Context) {
    ctx, cancelCtx := context.WithTimeout(ctx, 1500*time.Millisecond)
    defer cancelCtx()

    ...
}

...

更新并保存文件后,使用 go run 运行它:

go run main.go


输出将类似于以下内容:

Output
doAnother: 1
doAnother: 2
doAnother err: context deadline exceeded
doAnother: finished
doSomething: finished


这次运行程序时,您应该会看到与使用 context.WithDeadline 时相同的输出。错误消息甚至是相同的,向您显示上下文。WithTimeout 实际上只是一个包装器,用于计算时间。现在为您计算。

在本节中,您更新了程序以使用三种不同的方式结束 context.Context。第一个,context.WithCancel,允许你调用一个函数来取消上下文。接下来,您使用带有 time.Time 值的 context.WithDeadline 在特定时间自动结束上下文。最后,您使用 context.WithTimeout 和 time.Duration 值在经过一定时间后自动结束上下文。使用这些功能,您将能够确保您的程序不会消耗超过计算机所需的资源。了解它们导致上下文返回的错误也将使您自己和其他 Go 程序中的错误故障排除变得更加容易。

结论


在本教程中,您创建了一个程序来以各种方式与 Go 的 context 包进行交互。首先,您创建了一个接受 context.Context 值作为参数的函数,并使用 context.TODO 和 context.Background 函数创建空上下文。之后,您使用 context.WithValue 将值添加到新上下文并使用 Value 方法在其他函数中检索它们。最后,您使用了上下文的 Done 方法来确定上下文何时完成运行。当与函数 context.WithCancel、context.WithDeadline 和 context.WithTimeout 配对时,您实现了自己的上下文管理来设置使用这些上下文的代码应该运行多长时间的限制。

如果您想通过更多示例了解有关上下文如何工作的更多信息,Go 上下文包文档包含更多信息。

文章链接