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)
	}
}
1
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)
}

1
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()一样,一般做为占位符存在。

    img

当一个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,会引起数据竟态问题
}
1
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)
		}
	}
}
1
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)
		}
	}
}
1
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")
    }
}
1
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
 }
}
1
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())
	}
}
1
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显式的传递给需要的函数,否则可能会引起数据竞态,生命周期管理等问题