context包
并发是Go语言编程的特色,有效的管理并发对构建稳定健壮的程序至关重要,在Golang中,提供了context标准库,用于管理goroutine的生命周期,传递上下文,控制超时取消等操作。
我们先来看一个案例,体会一下context的作用:
假设我们需要从多个api接口中并发的获取数据,如果某个接口超时,就将其请求取消
func main() {
http.HandleFunc("/users", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("users"))
})
http.HandleFunc("/products", func(w http.ResponseWriter, r *http.Request) {
time.Sleep(5 * time.Second)
w.Write([]byte("products"))
})
http.HandleFunc("/orders", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("orders"))
})
err := http.ListenAndServe(":80", nil)
if err != nil {
log.Fatalln(err)
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
func main() {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
urls := []string{
"http://api.mszlu.com/users",
"http://api.mszlu.com/products",
"http://api.mszlu.com/orders",
}
results := make(chan string)
for _, url := range urls {
go fetchAPI(ctx, url, results)
}
for range urls {
fmt.Println(<-results)
}
}
func fetchAPI(ctx context.Context, url string, results chan string) {
req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
if err != nil {
results <- fmt.Sprintf("create request error,url=%s,err=%v", url, err)
return
}
client := http.DefaultClient
response, err := client.Do(req)
if err != nil {
results <- fmt.Sprintf("making request error,url=%s,err=%v", url, err)
return
}
defer response.Body.Close()
results <- fmt.Sprintf("response content, url=%s, status=%d", url, response.StatusCode)
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
1. 创建根context
context.Background()
:可以创建一个非nil
,空值
的Context对象,不能发出取消信号,线程安全,通常作为根Context
,用于派生其他Context。context.TODO()
: 和context.Background()
一样,一般做为占位符存在。
当一个context被取消后,其派生的context会同样被取消
context是线程安全的
2. value context
context可以在多个goroutines之间传值,
context.WithValue()
用来在context中存储键值对,它返回一个新的Context,这个新的Context携带了一个键值对。
func main() {
ctx := context.Background()
ctx = context.WithValue(ctx, "userId", 123)
//这样传参 可以避免显示传递值
go performTask(ctx)
time.Sleep(time.Second)
}
func performTask(ctx context.Context) {
userId := ctx.Value("userId")
fmt.Println("userId:", userId)
//ctx = context.WithValue(ctx, "userId", newValue) 多个协程中修改同一个key,会引起数据竟态问题
}
2
3
4
5
6
7
8
9
10
11
12
13
3. cancel Context
通过
context.WithCancel
创建的context可以发出取消信号。通过取消信号,我们可以终止相关的goroutines,从而避免资源泄露(未能正确释放导致无法回收)。
func main() {
ctx, cancel := context.WithCancel(context.Background())
go performTask(ctx)
time.Sleep(2 * time.Second)
cancel()
time.Sleep(1 * time.Second)
}
func performTask(ctx context.Context) {
for {
select {
case <-ctx.Done():
fmt.Println("Task cancelled")
return
default:
// Perform task operation
fmt.Println("Performing task...")
time.Sleep(500 * time.Millisecond)
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
context.WithoutCancel()
创建的context在parentContext被取消时,其不会被取消
func main() {
ctx, cancel := context.WithCancel(context.Background())
ctx = context.WithoutCancel(ctx)
go performTask(ctx)
time.Sleep(2 * time.Second)
cancel()
time.Sleep(5 * time.Second)
}
func performTask(ctx context.Context) {
for {
select {
case <-ctx.Done():
fmt.Println("Task cancelled")
return
default:
// Perform task operation
fmt.Println("Performing task...")
time.Sleep(500 * time.Millisecond)
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
4. Timeout Context
context.WithTimeout
用于创建一个带有超时的上下文,这个上下文会在指定的超时时间之后自动取消
context有四个方法:
- Deadline() : 返回超时的截止时间
- Done(): 上下文完成时或者取消后会调用
- Err():Done()返回后,使用Err可以获得结束原因
- Value():获取键值
**注意:**如果父
context
有一个截止时间,那么子context
的截止时间将是父context
的截止时间和子context
自己的截止时间中较早的那个。
func main() {
// 创建一个带有超时的上下文
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel() // 在程序结束前调用 cancel 函数释放资源
// 启动一个任务
go task(ctx)
// 等待一段时间,超时后任务会被取消
time.Sleep(3 * time.Second)
fmt.Println("Main goroutine: Done")
}
func task(ctx context.Context) {
select {
case <-time.After(3 * time.Second):
fmt.Println("Task: Finished")
case <-ctx.Done():
fmt.Println("Task: Context cancelled or timed out")
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
context.WithTimeoutCause()
和context.WithTimeout()
区别就是错误信息可以自定义
4. Deadline Context
context.WithDeadline()
和context.WithTimeout
一样,区别在于时间的表示方式,context.WithDeadline()
时间需要时一个具体的时刻
func main() {
//适合用在需要在某个时间之前终止操作的场景
ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(2*time.Second))
defer cancel()
go performTask(ctx)
time.Sleep(3 * time.Second)
}
func performTask(ctx context.Context) {
select {
case <-ctx.Done():
fmt.Println("Task completed or deadline exceeded:", ctx.Err())
return
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
context.WithDeadlineCause()
这个函数和WithDeadline
用法一样,唯一的区别是可以自定义错误信息。
5. AfterFunc
context.AfterFunc
是在parent contenxt
完成或者取消后,执行一个函数,会返回一个stop
函数,用于停止parent contenxt
和func的关联,返回true代表成功取消,false代表context已经完成或者取消。
func main() {
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel() // 确保在函数结束时取消context
// 设置一个2秒后执行的函数
stop := context.AfterFunc(ctx, func() {
fmt.Println("AfterFunc executed")
})
go performTask(ctx, stop)
// 阻塞主goroutine,防止程序立即退出
time.Sleep(3 * time.Second)
}
func performTask(ctx context.Context, stop func() bool) {
select {
case <-ctx.Done():
fmt.Println("Context canceled:", ctx.Err())
fmt.Println("stop:", stop())
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
6. 注意的一些问题
不传递上下文:
如果一个函数创建了上下文,但并没有传递给其他函数或协程,这些函数和协程无法响应上下文相关的处理操作。忘记调用取消函数:
使用有取消函数的上下文时,记得取消协程泄露:
有上下文的协程需要检查Done channel,当接收到到信号时及时清理资源和退出过度使用context.Background:
没有取消和超时功能,很可能引起问题传递nil的上下文:
不要传递nil的上下文,会导致panic阻塞调用:
应该将阻塞操作(比如IO)包装成使用上下文检查的调用,可以避免被挂起过度使用上下文:
上下文也并不是所有场景都使用,比如处理全局资源或者共享状态等,可能更使用使用锁或者channel上下文存储在结构体中
:应该将context显式的传递给需要的函数,否则可能会引起数据竞态,生命周期管理等问题