Context
Context是Go语言中一个用于传递请求范围的上下文信息的标准库包,其主要用于处理并发操作中请求的生命周期的管理。
协程如何退出
利用协程退出的例子来说明Context的作用,以及没有使用Context,应该如何在没有执行完代码时提前退出协程
go
package main
import (
"fmt"
"time"
)
func main() {
// 创建一个用于退出的信号 channel
exitChan := 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库进行改造
go
package main
import (
"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同时处理多个协程
go
package main
import (
"context"
"fmt"
"sync"
"time"
)
func main() {
// 创建一个带取消功能的上下文
ctx, cancel := context.WithCancel(context.Background())
defer cancel() // 确保在 main 结束时调用取消
var wg sync.WaitGroup
// 启动多个协程
numGoroutines := 3
for i := 1; i <= numGoroutines; i++ {
wg.Add(1) // 增加 WaitGroup 计数
go func(id int, ctx context.Context) {
defer wg.Done() // 协程完成时调用 Done
for {
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种
go
1. Deadline() (deadline time.Time,ok bool)
// 这个方法可以获取设置的截止时间,第一个返回值deadline为截止时间,到了这个时间点,Context会自动发起取消请求,第二个返回值ok表示是否设置了截止时间
2. Done() <-chan struct{}
// 这个方法返回一个只读的通道,当上下文被取消时,这个通道就会被关闭。当方法返回的chan可以读取时,则意味着Context已经发起了取消信号。通过Done方法收到这个信号之后,就可以做清理操作,然后退出协程,释放资源
3. Err() error
// 这个方法返回上下文的错误状态,如果上下文被取消,返回context.Canceled;如果超时返回context.DeadlineExceeded
4. Value(key interface{}) interface{}
// 从上下文中获取与特定键关联的值,Value方法获取该Context上绑定的值,是一个键值对,所以要通过Key才可以获取。
上下文的创建(Context树)
上下文树的基本结构:
根上下文:通常使用context.Background() 或 context.TODO() 作为树的根节点。
子上下文:通过context.WithCancel(), context.WithTimeout(), 或 context.WithDeadline()创建的上下文是根上下文或其他上下文的子上下文。
上下文函数详解
Context主要提供了5种方法来创建新的上下文:
go
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树中,父上下文的状态会影响到所有子上下文,当父上下文被取消是,所以的子上下嗯也会自动被取消。
go
package main
import (
"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{}类型实现
go
package main
import (
"context"
"fmt"
)
type key string
const 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 {}
}