Context
Context是Go语言中一个用于传递请求范围的上下文信息的标准库包,其主要用于处理并发操作中请求的生命周期的管理。
协程如何退出
利用协程退出的例子来说明Context的作用,以及没有使用Context,应该如何在没有执行完代码时提前退出协程
package mainimport ("fmt""time"
)func main() {// 创建一个用于退出的信号 channelexitChan := make(chan struct{})// 启动一个协程go func() {for {select {case <-exitChan:fmt.Println("协程收到退出信号,正在退出...")return // 退出协程default:// 模拟一些工作fmt.Println("协程正在执行...")time.Sleep(1 * time.Second) // 假装在做事情}}}()// 主协程等待一段时间后发送退出信号time.Sleep(5 * time.Second)close(exitChan) // 发送退出信号// 等待一段时间,确保协程能够退出time.Sleep(1 * time.Second)fmt.Println("主协程结束")
}
这段代码使用了for select循环来中途暂停协程运行
当我们启动了一个处主协程之外的协程时,我们可以通过for select循环来选择停止协程与继续协程
虽然这段代码看上去并不长,并且十分好用,但现实中肯定不止这一个协程,如果想同时让很多个协程停止那么代码将会很长,所以这时就要使用Context了。
Context使用示例
将上面的代码使用Context库进行改造
package mainimport ("context""fmt""time"
)func main() {// 创建一个带取消功能的上下文ctx, cancel := context.WithCancel(context.Background())defer cancel() // 确保在 main 结束时调用取消// 启动一个协程go func(ctx context.Context) {for {select {case <-ctx.Done():fmt.Println("协程收到退出信号,正在退出...")return // 退出协程default:// 模拟一些工作fmt.Println("协程正在执行...")time.Sleep(1 * time.Second) // 假装在做事情}}}()// 主协程等待一段时间后发送退出信号time.Sleep(5 * time.Second)cancel() // 发送退出信号// 等待一段时间,确保协程能够退出time.Sleep(1 * time.Second)fmt.Println("主协程结束")
}
在这段代码中使用 context.WithCancel创建一个可取消的上下文,其中cancel函数用于取消上下文。
当cancel函数被调用时,ctx.Done()会接收到结束的信号,并传递给case让进程结束。
Context同时处理多个协程
package mainimport ("context""fmt""sync""time"
)func main() {// 创建一个带取消功能的上下文ctx, cancel := context.WithCancel(context.Background())defer cancel() // 确保在 main 结束时调用取消var wg sync.WaitGroup// 启动多个协程numGoroutines := 3for i := 1; i <= numGoroutines; i++ {wg.Add(1) // 增加 WaitGroup 计数go func(id int, ctx context.Context) {defer wg.Done() // 协程完成时调用 Donefor {select {case <-ctx.Done():fmt.Printf("协程 %d 收到退出信号,正在退出...\n", id)return // 退出协程default:// 模拟一些工作fmt.Printf("协程 %d 正在执行...\n", id)time.Sleep(1 * time.Second) // 假装在做事情}}}(i, ctx)}// 主协程等待一段时间后发送退出信号time.Sleep(5 * time.Second)cancel() // 发送退出信号// 等待所有协程完成wg.Wait()fmt.Println("所有协程已结束,主协程结束")
}
Context详解
Context接口
Context接口方法主要有4种
1. Deadline() (deadline time.Time,ok bool)
// 这个方法可以获取设置的截止时间,第一个返回值deadline为截止时间,到了这个时间点,Context会自动发起取消请求,第二个返回值ok表示是否设置了截止时间2. Done() <-chan struct{}
// 这个方法返回一个只读的通道,当上下文被取消时,这个通道就会被关闭。当方法返回的chan可以读取时,则意味着Context已经发起了取消信号。通过Done方法收到这个信号之后,就可以做清理操作,然后退出协程,释放资源3. Err() error
// 这个方法返回上下文的错误状态,如果上下文被取消,返回context.Canceled;如果超时返回context.DeadlineExceeded4. Value(key interface{}) interface{}
// 从上下文中获取与特定键关联的值,Value方法获取该Context上绑定的值,是一个键值对,所以要通过Key才可以获取。
上下文的创建(Context树)
上下文树的基本结构:
根上下文:通常使用context.Background() 或 context.TODO() 作为树的根节点。
子上下文:通过context.WithCancel(), context.WithTimeout(), 或 context.WithDeadline()创建的上下文是根上下文或其他上下文的子上下文。
上下文函数详解
Context主要提供了5种方法来创建新的上下文:
1. context.Background()
// 返回一个空上下文,通常做根上下文,通常在程序的最顶层使用,它可以作为其他上下文的父上下文2. context.TODO()
// 当不确定使用哪个上下文时,可以使用TODO(),这个上下文的用途通常在代码开发的过程中,表示你需要稍后处理的上下文。3. context.WithCancel(parent Context)
// 创建一个可取消的上下文,返回一个新上下文和一个取消函数。调用取消函数会取消这个上下文及其所以子上下文。4. context.WithTimeout(parent Context,timeout time.Duration)
//创建一个带有超时的上下文。当超时时间达到,自动取消上下文5. context.WithDeadline(parent Context,deadline time.Time)
// 与WithTimeout类似,但是使用绝对时间来设置截止时间
Context树的传播
在Context树中,父上下文的状态会影响到所有子上下文,当父上下文被取消是,所以的子上下嗯也会自动被取消。
package mainimport ("context""fmt""time"
)func main() {// 创建根上下文rootCtx, cancel := context.WithCancel(context.Background())defer cancel()// 创建子上下文childCtx, childCancel := context.WithTimeout(rootCtx, 2*time.Second)defer childCancel()go func(ctx context.Context) {select {case <-ctx.Done():fmt.Println("子上下文被取消:", ctx.Err())}}(childCtx)// 模拟一些工作time.Sleep(1 * time.Second)// 取消根上下文cancel()// 等待子协程结束time.Sleep(1 * time.Second)
}
Context传值
Context在go中的作用不仅可以用于取消协程,还可以传值,通过这个能力Context储存的值可以供其他协程使用,这个方式适合传递请求范围内的共享数据。
context的值是通过context.WithValue函数设置的,其中传递的值是不可变的,并且使用interface{}类型实现
package mainimport ("context""fmt"
)type key stringconst userKey key = "user"func main() {// 创建一个背景上下文ctx := context.Background()// 将值存入上下文ctx = context.WithValue(ctx, userKey, "Alice")// 在 goroutine 中使用上下文go func(ctx context.Context) {// 从上下文中获取值if user, ok := ctx.Value(userKey).(string); ok {fmt.Println("User from context:", user)} else {fmt.Println("User not found in context")}}(ctx)// 等待 goroutine 完成// 在实际应用中,使用 sync.WaitGroup 或其他同步机制select {}
}