“软件包上下文定义了上下文类型,它跨 API 边界和进程之间携带截止日期、取消信号和其他请求范围的值。”- golang.org
Hello folks,今天我们介绍一下 Golang 语言中 Context 包相关概念特性及编程实践,这里我们以最新的 v1.18.2 为例。
Context 是 Golang 标准库中的一个核心包,其提供了 goroutine 之间的传递信息的机制、信号同步,除此之外,其还具备超时( Timeout )和取消( Cancel )机制。总而言之,一句话总结:Context 控制子 goroutine 的运行,超时控制及取消的方法调用。
在解析 Context 之前,让我们先来看个 Demo,在下面的示例中,我们模拟一个简单的 HTTP 服务器来处理请求,具体如下所示:
[leonli@192 context ] % vi main.go package main import ( "fmt" "net/http" "time" ) func demoRequest(w http.ResponseWriter, req *http.Request) { fmt.Println("Hi girl, I love you !") context := req.Context() select { // Simulating some work by the server, waits 5 seconds and then responds. case <-time.After(5 * time.Second): fmt.Fprintf(w, "Hi man, I love you too!") // Handling request cancellation case <-context.Done(): err := context.Err() fmt.Println("Error:", err) } fmt.Println("Let's go !") } func main() { http.HandleFunc("/request", demoRequest) fmt.Println("Confession is running...") http.ListenAndServe(":4000", nil) }
此时,我们打开 2 个窗体,在窗体 1 中,运行 Go 程序,具体如下:
[leonli@192 context ] % go run main.go Confession is running...
然后,我们跳转至窗体 2 运行客户端请求,如下所示:
[leonli@192 context ] % curl localhost:4000/request
此时,我们查看窗体 1 ,发现输出:“Hi girl, I love you !” ,具体如下:
[leonli@192 context ] % go run main.go Confession is running... Hi girl, I love you !
等待数秒后,窗体 2 输出:“Hi man, I love you too! ” ,如下所示:
[leonli@192 context ] % curl localhost:4000/request Hi man, I love you too!
然后,再一次回到窗体 1 ,我们将会看到如下:
[leonli@192 context ] % go run main.go Confession is running... Hi girl, I love you ! Let's go !
在 Golang 中,传入服务器的请求应创建上下文,向服务器发出的调用应接受上下文。它们之间的函数调用链基于上下文传播,可以选择将其替换为使用 WithCancel、WithDeadline、WithTimeout 或 WithValue 创建的派生上下文。当上下文被取消时,从中派生的所有上下文也会被取消。
WithCancel、WithDeadline 和 WithTimeout 函数采用 Context(父)并返回派生 Context(子)和 aCancelFunc。调用 CancelFunc 将取消子项及其子项,删除父项对父项的引用,并停止任何关联的计时器。未能调用 CancelFunc 会泄露子及其子项,直到父项被取消或计时器触发。
从本质上讲,Context 是一种 Interface 类型,其结构定义如下:
type Context interface { Deadline() (deadline time.Time, ok bool) Done() <-chan struct{} Err() error Value(key any) any }
具体来说,Context 类型通常具有如下方法:
1、Done() <- chan struct{} 返回上下文取消或超时时关闭的通道。若上下文永远无法取消,则完成可能会返回 nil。
2、Deadline() (deadline time.Time, ok bool) 返回上下文被取消或超时的时间。当没有设定截止日期时,截止日期返回为 false。
3、Err() error 返回一个错误,即解释了 Done 通道被关闭的原因。如果尚未关闭,它将返回 nil。
4、Value(key any) any 返回与键关联的值,若无,则返回 nil。
接下来,我们简要描述下 Context 所包含的功能特性。
func WithValue
func WithValue(parent Context, key, val any) Context
此函数接受上下文并返回派生上下文,其中值 val 与 key 相关联,并与上下文一起流经上下文树。
这意味着,一旦获得具有值的上下文,由此派生的任何上下文都会获得此值。通常而言,不建议使用上下文值传递关键参数,相反,函数应该在签名中接受这些显式值。
如下示例演示了如何将值传递到上下文,以及如果存在该值,如何检索它。
[leonli@192 context ] % vi main-withvalue.go package main import ( "context" "fmt" ) func main() { type favContextKey string f := func(ctx context.Context, k favContextKey) { if v := ctx.Value(k); v != nil { fmt.Println("found value:", v) return } fmt.Println("key not found:", k) } k := favContextKey("language") ctx := context.WithValue(context.Background(), k, "9527") f(ctx, k) f(ctx, favContextKey("That guy is not here !")) } Output: found value: 9527 key not found: That guy is not here !
func WithCancel
func WithCancel(parent Context) (ctx Context, cancel CancelFunc)
此函数从父上下文和派生上下文以及取消函数创建新上下文。父级可以是 context.Background 或传递到函数中的上下文。
取消此上下文会释放与之关联的资源,因此代码应在此上下文中运行的操作完成后立即调用取消。通常情况下,不建议传递 Cancel 功能,因为它可能会导致意外行为。如下示例演示了使用可取消上下文来防止程序泄漏。具体可参考如下示例:
[leonli@192 context ] % vi main-withcancel.go package main import ( "runtime" "fmt" "time" "context" ) func thread2(ctx context.Context, number int) { for { select { case v := <- ctx.Done(): fmt.Printf("thread: %v, the received channel value is: %v, ending\n", number,v) return default: fmt.Printf("thread: %v here!\n", number) time.Sleep(2 * time.Second) } } } func thread1(ctx context.Context, number int) { for { go thread2(ctx, number) select { case v := <- ctx.Done(): fmt.Printf("thread: %v, too far to go: %v, ending\n", number, v) return default: fmt.Printf("thread: %v here!\n", number) time.Sleep(2 * time.Second) } } } func main() { var ctx context.Context = nil var cancel context.CancelFunc = nil ctx, cancel = context.WithCancel(context.Background()) for i := 1; i <= 5; i = i + 1 { go thread1(ctx, i) } time.Sleep(1 * time.Second) // close all gourtines cancel() // waiting 10 seconds, if the screen does not display <thread: xxxx in progress>, all goroutines have been shut down time.Sleep(10 * time.Second) println(runtime.NumGoroutine()) println("Fucking, stop playing!") } Output: thread: 2 here! thread: 1 here! thread: 3 here! thread: 2 here! thread: 5 here! thread: 3 here! thread: 1 here! thread: 5 here! thread: 4 here! thread: 4 here! thread: 5, too far to go: {}, ending thread: 4, too far to go: {}, ending thread: 5, too far to go: {}, ending thread: 3, too far to go: {}, ending thread: 4, too far to go: {}, ending thread: 3, too far to go: {}, ending thread: 4, too far to go: {}, ending thread: 1, too far to go: {}, ending thread: 5, too far to go: {}, ending thread: 1, too far to go: {}, ending thread: 2, too far to go: {}, ending thread: 2, too far to go: {}, ending thread: 2, too far to go: {}, ending thread: 1, too far to go: {}, ending thread: 3, too far to go: {}, ending 1 Fucking, stop playing!
func WithDeadline
func WithDeadline(parent Context, d time.Time) (Context, CancelFunc)
此函数返回其父函数的派生上下文,当截止日期超过或调用取消函数时,该上下文会被取消。
例如,我们可以创建一个上下文,该上下文将在未来某个时间自动被取消,并将其传递给子函数。当该上下文因截止日期即将到期而被取消时,所有获得上下文的功能都会收到停止工作并返回的通知。具体可参考如下:
[leonli@192 context ] % vi main-withdeadline.go package main import ( "runtime" "fmt" "time" "context" ) func thread2(ctx context.Context, index int) { for { select { case v := <- ctx.Done(): fmt.Printf("thread2: %v, too far to go: %v, ending\n", index, v) return default: fmt.Printf("thread2: %v here\n", index) time.Sleep(2 * time.Second) } } } func thread1(ctx context.Context, index int) { for { go thread2(ctx, index) select { case v := <- ctx.Done(): // this branch is only reached when the ch channel is closed, or when data is sent(either true or false) fmt.Printf("thread1: %v, too far to go: %v, ending\n", index, v) return default: fmt.Printf("thread1: %v here\n", index) time.Sleep(2 * time.Second) } } } func main() { var ctx01 context.Context = nil var ctx02 context.Context = nil var cancel context.CancelFunc = nil ctx01, cancel = context.WithCancel(context.Background()) ctx02, cancel = context.WithDeadline(ctx01, time.Now().Add(1 * time.Second)) // If it's WithTimeout, just change this line to "ctx02, cancel = context.WithTimeout(ctx01, 1 * time.Second)" defer cancel() for i := 1; i <= 5; i = i + 1 { go thread1(ctx02, i) } time.Sleep(5 * time.Second) if ctx02.Err() != nil { fmt.Println("the cause of cancel is: ", ctx02.Err()) } println(runtime.NumGoroutine()) println("Fucking, stop playing!") } Output: thread1: 1 here thread1: 5 here thread1: 2 here thread1: 4 here thread2: 5 here thread2: 1 here thread2: 4 here thread2: 2 here thread1: 3 here thread2: 3 here thread1: 3, too far to go: {}, ending thread1: 4, too far to go: {}, ending thread2: 3, too far to go: {}, ending thread2: 3, too far to go: {}, ending thread2: 2, too far to go: {}, ending thread1: 1, too far to go: {}, ending thread2: 5, too far to go: {}, ending thread2: 1, too far to go: {}, ending thread2: 4, too far to go: {}, ending thread1: 5, too far to go: {}, ending thread2: 5, too far to go: {}, ending thread2: 4, too far to go: {}, ending thread2: 1, too far to go: {}, ending thread1: 2, too far to go: {}, ending thread2: 2, too far to go: {}, ending the cause of cancel is: context deadline exceeded 1 Fucking, stop playing!
func WithTimeout
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc) { return WithDeadline(parent, time.Now().Add(timeout)) }
此函数通常只是带有额外超时的 WithDeadline 函数的包装器。具体功能实现可参考如上代码所示。
至此,针对 Context 的相关功能解析及实践 Demo 先解析到此为止,希望大家有所收获!
Adiós !