Context意思为”上下文”,主要的作用是用来在goroutine之间传递上下文的信息。上下文包含的功能有:
上下文是可以进行传播的,也就是说通过调用一些方法可以由父级派生出子级
Context的功能可以从两个方面去体现:
简单来说,我们需要一些终止信号,来告诉我们的程序去停止进行一些不必要的工作。
用一个简单的例子来说明:
从客户端发送请求到程序处理再到数据库中,正常流程如下图所示
但是当客户端发送请求后,如果断开链接,那么正常情况下后序的数据库查询操作应该停止,如果没有任何中止信号那么程序会继续执行,但是正常的情况是需要终止继续操作
所以我们就知道为什么需要终止信号了
package main
import (
"fmt"
"net/http"
"os"
"time"
"context"
)
type ContextKey string
func main() {
// 测试方法
// 浏览器打开localhost:8000
// 两秒之后会返回request processed
// 如果发送请求后立刻断开那么 ctx.Done() 能够收到信号 执行中断
http.HandleFunc("/",handlerOne)
//测试timeout和cancel
http.HandleFunc("/timeout",handlerTwo)
http.ListenAndServe(":8000", nil)
}
func handlerOne(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
fmt.Fprint(os.Stdout, "processing request\n")
select {
case <-time.After(2 * time.Second):
// 2秒后要是收到信号
// 说明请求被执行
w.Write([]byte("request processed"))
case <-ctx.Done():
// 如果请求被取消 输出请求被取消
fmt.Fprintln(os.Stderr, "request cancelled")
fmt.Println(ctx.Err())
}
}
func handlerTwo(w http.ResponseWriter, r *http.Request) {
ctx, cancel := context.WithTimeout(r.Context(), 5*time.Second)
ctx = context.WithValue(ctx, ContextKey("Ethan"), "Hi Leo")
defer cancel()
c := make(chan string)
go func() {
getFromDb(ctx, c)
}()
select {
case <-ctx.Done():
w.Write([]byte(ctx.Err().Error()))
case data := <-c:
w.Write([]byte(data))
}
}
func getFromDb(ctx context.Context, c chan<- string) {
time.Sleep(6 * time.Second)
value := ctx.Value(ContextKey("Ethan"))
v, ok := value.(string)
if !ok {
c <- "get data error"
return
}
c <- v
}
ctx:=context.Background()
该方法返回一个非零的空上下文。它永远不会被取消,没有价值,也没有截止日期。它通常由 main 函数、初始化和测试使用,并作为传入请求的顶级上下文。
ctx:=context.TODO()
该方法返回一个非零的空上下文。如果实在不清楚需要传递什么,可以使用该方法
func WithValue(parent Context, key, val interface{}) Context 1.18以前的版本函数签名是这样的
官方建议尽量别使用内置类型
The provided key must be comparable and should not be of type string or any other built-in type to avoid collisions between packages using context. Users of WithValue should define their own types for keys. To avoid allocating when assigning to an interface{}, context keys often have concrete type struct{}. Alternatively, exported context key variables’ static type should be a pointer or interface.
该方法会返回一个context的副本,返回的ctx中会携带key,val的信息
type contextKey string
ctx:=context.Background()
k:=contextKey("Ethan")
ctx = context.WithValue(ctx,k,"Leo")
示例
type contextKey string
func WithValue() {
k := contextKey("Ethan")
ctx := context.Background()
f(ctx)
ctx = context.WithValue(ctx, k, "Leo")
f(ctx)
}
func f(ctx context.Context) {
if value := ctx.Value(contextKey("Ethan")); value != nil {
fmt.Printf("value is %v\n", value)
return
}
fmt.Println("no value")
}
//$ go test -v -run TestWithValue
//=== RUN TestWithValue
//=== RUN TestWithValue/test_with_value
//no value
//value is Leo
该方法会返回一个context副本,和一个取消函数,参数timeout的设置,会让ctx在指定的时间段时候,接收到ctx.Done()的信号,用来关闭context
var c chan struct{}
func WithTimeOut() {
c = make(chan struct{})
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()
go f(ctx)
time.Sleep(6 * time.Second)
//time.Sleep(1 * time.Second)
close(c)
time.Sleep(1 * time.Second)
}
func f(ctx context.Context) {
select {
case <-c:
fmt.Println("main goroutine is done")
case <-ctx.Done():
fmt.Println("context is time out")
}
}
WithDeadline 返回父上下文的副本,截止日期调整为不晚于 d。如果父级的截止日期已经早于 d,则 WithDeadline(parent, d) 在语义上等同于父级。当截止日期到期、调用返回的取消函数或父上下文的 Done 通道关闭时,返回的上下文的 Done 通道将关闭,以先发生者为准
示例
package with_daedline
import (
"context"
"fmt"
"time"
)
func WithDeadLine() {
deadline := time.Now().Add(10 * time.Second)
ctx, cancel := context.WithDeadline(context.Background(), deadline)
defer cancel()
c := make(chan string)
go readFile(c)
go getDb(c)
select {
case <-ctx.Done():
fmt.Println(ctx.Err().Error())
case data := <-c:
fmt.Println(data)
}
}
func readFile(c chan<- string) {
time.Sleep(11 * time.Second)
c <- "data from file"
}
func getDb(c chan<- string) {
time.Sleep(13 * time.Second)
c <- "data from db"
}
- WithCancel()函数接受一个 Context 并返回其子Context和取消函数cancel
- 新创建协程中传入子Context做参数,且需监控子Context的Done通道,若收到消息,则退出
- 需要新协程结束时,在外面调用 cancel 函数,即会往子Context的Done通道发送消息
- 注意:当 父Context的 Done() 关闭的时候,子 ctx 的 Done() 也会被关闭
示例
- 利用根Context创建一个父Context,使用父Context创建一个协程,
- 利用上面的父Context再创建一个子Context,使用该子Context创建一个协程
- 一段时间后,调用父Context的cancel函数,会发现父Context的协程和子Context的协程都收到了信号,被结束了
func WithCancelTwo() {
ctx, cancel := context.WithCancel(context.Background())
// 父context的子协程
go watch1(ctx)
// 子context,注意:这里虽然也返回了cancel的函数对象,但是未使用
valueCtx, _ := context.WithCancel(ctx)
// 子context的子协程
go watch2(valueCtx)
fmt.Println("现在开始等待3秒,time=", time.Now().Unix())
time.Sleep(3 * time.Second)
// 调用cancel()
fmt.Println("等待3秒结束,调用cancel()函数")
cancel()
// 再等待5秒看输出,可以发现父context的子协程和子context的子协程都会被结束掉
time.Sleep(5 * time.Second)
fmt.Println("最终结束,time=", time.Now().Unix())
}
// 父context的协程
func watch1(ctx context.Context) {
for {
select {
case <-ctx.Done(): //取出值即说明是结束信号
fmt.Println("收到信号,父context的协程退出,time=", time.Now().Unix())
return
default:
fmt.Println("父context的协程监控中,time=", time.Now().Unix())
time.Sleep(1 * time.Second)
}
}
}
// 子context的协程
func watch2(ctx context.Context) {
for {
select {
case <-ctx.Done(): //取出值即说明是结束信号
fmt.Println("收到信号,子context的协程退出,time=", time.Now().Unix())
return
default:
fmt.Println("子context的协程监控中,time=", time.Now().Unix())
time.Sleep(1 * time.Second)
}
}
}
//$ go test -v -run TestWithCancelTwo
//=== RUN TestWithCancelTwo
//=== RUN TestWithCancelTwo/test_with_cancel
//现在开始等待3秒,time= 1671882383
//父context的协程监控中,time= 1671882383
//子context的协程监控中,time= 1671882383
//子context的协程监控中,time= 1671882384
//父context的协程监控中,time= 1671882384
//父context的协程监控中,time= 1671882385
//子context的协程监控中,time= 1671882385
//子context的协程监控中,time= 1671882386
//等待3秒结束,调用cancel()函数
//父context的协程监控中,time= 1671882386
//收到信号,子context的协程退出,time= 1671882387
//收到信号,父context的协程退出,time= 1671882387
//最终结束,time= 1671882391