在这篇文章中,我们将了解 Go 的 context 包简单使用方法。
取消上下文
为什么我们需要取消?
HTTP 服务器调用数据库并将查询的数据返回给客户端是一个常见业务场景,但如果客户端在中途取消请求会怎样?例如,如果客户端在请求中途关闭浏览器。此时,我们应该立即取消后续的执行处理,以防止我们的系统做不必要的工作。
上下文取消有两个方面:
- 监听取消事件
- 发出取消事件
监听取消事件
Context
类型提供了一个 Done()
方法。每次上下文收到取消事件时,这个方法都会返回一个接收空 struct{}
类型的 channel。
因此,要监听取消事件,我们需要等待 <- ctx.Done()
。
例如,假设一个 HTTP 服务器需要两秒钟来处理一个事件。如果请求在此之前被取消,我们希望立即返回:
package main
import (
"fmt"
"net/http"
"os"
"time"
)
func main() {
// 创建一个监听 8000 端口的 HTTP 服务器
http.ListenAndServe(":8000", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
// 这会打印到 STDOUT 以表明处理已经开始
fmt.Fprint(os.Stdout, "processing request\n")
// 我们使用 select 来处理事件,事件取决于哪个通道先接收到消息
select {
case <-time.After(2 * time.Second):
// 如果我们在2秒后收到消息,表示请求已经被处理
w.Write([]byte("request processed"))
case <-ctx.Done():
// 如果请求被取消,将其记录到STDERR
fmt.Fprint(os.Stderr, "request cancelled\n")
}
}))
}
你可以通过运行服务器并在浏览器上打开 https://localhost:8000 来对此进行测试。如果你在 2 秒之前关闭浏览器,你应该会在终端窗口上看到“request cancelled”字样。
发出取消事件
可以通过上下文发出取消事件完成取消操作,这可以通过使用 context 包中的 WithCancel
函数来完成,它返回一个上下文对象和一个函数。
ctx, fn := context.WithCancel(ctx)
返回的这个函数不接受任何参数,也不返回任何内容,当你需要取消上下文时就调用它。下面是一个示例
package main
import (
"context"
"errors"
"fmt"
"time"
)
func operation1(ctx context.Context) error {
// 假设这个操作由于某种原因失败了返回 error
// 我们使用 time.Sleep 来模拟资源密集型操作
time.Sleep(100 * time.Millisecond)
return errors.New("failed")
}
func operation2(ctx context.Context) {
// 我们使用与前面示例中看到的 HTTP 服务器类似的模式
select {
case <-time.After(500 * time.Millisecond):
fmt.Println("done")
case <-ctx.Done():
fmt.Println("halted operation2")
}
}
func main() {
// 创建一个新的上下文
ctx := context.Background()
// 创建一个新的上下文,它的取消函数来自于原来的上下文
ctx, cancel := context.WithCancel(ctx)
// 运行两个操作:一个在不同的 goroutine 中
go func() {
err := operation1(ctx)
// 如果此操作返回错误,则取消使用此上下文的所有操作
if err != nil {
cancel()
}
}()
// operation2使用与operation1相同的上下文
operation2(ctx)
}
运行代码,输出结果:
halted operation2
上下文超时
API 与前面的示例几乎相同,只是增加了一些内容:
// 该上下文将在3秒后取消
// 如果需要提前取消,可以像前面一样使用' cancel '函数
ctx, cancel := context.WithTimeout(ctx, 3*time.Second)
// 设置上下文 Deadline 与设置超时类似,只是指定了希望上下文取消的具体时间点,而不是指定时长。
// 此处,上下文将在2009-11-10 23:00:00被取消
ctx, cancel := context.WithDeadline(ctx, time.Date(2009, time.November, 10, 23, 0, 0, 0, time.UTC))
例如,对外部的 HTTP API 进行调用时,如果耗时太长,最好提前失败并取消请求:
package main
import (
"context"
"fmt"
"net/http"
"time"
)
func main() {
// 创建一个新的上下文
// 截止时间为 100 毫秒
ctx := context.Background()
ctx, cancel := context.WithTimeout(ctx, 100*time.Millisecond)
defer cancel()
// 发出请求,调用 blog.axiaoxin.com 博客主页
req, _ := http.NewRequest(http.MethodGet, "https://blog.axiaoxin.com", nil)
// 将我们刚刚创建的可取消上下文关联到请求
req = req.WithContext(ctx)
// 创建一个新的 HTTP 客户端并执行请求
client := &http.Client{}
res, err := client.Do(req)
// 如果请求失败,记录到 STDOUT
if err != nil {
fmt.Println("Request failed:", err)
return
}
// 请求成功打印状态码
fmt.Println("Response received, status code:", res.StatusCode)
}
根据博客主页对你请求的响应速度,你将收到:
Response received, status code: 200
或
Request failed: Get https://blog.axiaoxin.com: context deadline exceeded
上下文传值
你可以使用上下文变量来传递通用的值。这是比在所有函数调用中将它们作为变量传递的更惯用方法。
例如,考虑一个具有多个函数调用的操作,使用一个公共 ID 来标识它以进行日志记录和监控。
实现这个的最简单的方法是为每个函数调用传递 ID:
func main() {
// 创建一个随机整数作为 ID
rand.Seed(time.Now().Unix())
id := rand.Int63()
operation1(id)
}
func operation1(id int64) {
// do some work
log.Println("operation1 for id:", id, " completed")
operation2(id)
}
func operation2(id int64) {
// do some work
log.Println("operation2 for id:", id, " completed")
}
运行代码,输出:
2009/11/10 23:00:00 operation1 for id: 767100843235198854 completed
2009/11/10 23:00:00 operation2 for id: 767100843235198854 completed
为什么 Go 的 rand 包中返回一个 64 位整数时称为
Int63
?
Int63
方法从默认 Source 返回一个非负伪随机 63 位整数作为 int64。
int64
是 64 位有符号整数类型。它包含 1 个符号位和 63 个有效位。因此任何返回非负
int64
都会产生 63 位数据(第 64 位,符号位,将始终具有相同的值)。
当你想要传递更多信息时,参数很快就会变得臃肿。
我们可以使用上下文实现相同的功能:
// 我们需要设置一个键来告诉我们数据存储在哪里
const keyID = "id"
func main() {
rand.Seed(time.Now().Unix())
ctx := context.WithValue(context.Background(), keyID, rand.Int63())
operation1(ctx)
}
func operation1(ctx context.Context) {
// do some work
// 我们可以通过传入键从上下文中获取值
log.Println("operation1 for id:", ctx.Value(keyID), " completed")
operation2(ctx)
}
func operation2(ctx context.Context) {
// do some work
// 相同的 ID 从一个函数调用传递到下一个函数调用
log.Println("operation2 for id:", ctx.Value(keyID), " completed")
}
使用上下文变量传递信息很有用,原因有很多:
- 它是线程安全的:上下文键的值一旦设置就无法修改。为给定键设置另一个值的唯一方法是使用 context.WithValue 创建另一个上下文变量
- 它是传统手艺:context 包在整个 Go 的官方库和应用程序中使用,以传递操作范围的数据。其他开发人员和库通常可以很好地使用这种模式。
陷阱和注意事项
使用 WithTimeout
或 WithCancel
包装一个_可取消的上下文_将会使代码中的多个位置可以取消上下文,应该避免这种情况。