目录

Context使用说明

概述

context主要用来在goroutine之间传递上下文信息,包括:取消信号、超时时间、截止时间、k-v等。 在Goroutine构成的树形结构中对信号进行同步以减少计算资源的浪费context.Context的最大作用。

那其实很直观的就会有个疑问,Go中怎么通过传递ctx实现超时控制的呢?我们可以看下面两个示例:

示例:系统代码使用ctx

这段代码来源于src/database/sql.go,大致在1336行。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
// 阻塞从req中获取链接,如果超时,直接返回
select {
case <-ctx.Done():
  // 获取链接超时了,直接返回错误
  // do something
  return nil, ctx.Err()
case ret, ok := <-req:
  // 拿到链接,校验并返回
  return ret.conn, ret.err
}
官方示例WebSearch

示例链接:https://go.dev/blog/context

示例中将代码分成了三部分:

  • server 主调
  • userip 用来给ctx传递KV做示例
  • google 用来展示使用ctx进行超时控制
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
// google 
func httpDo(ctx context.Context, req *http.Request, f func(*http.Response, error) error) error {
    // Run the HTTP request in a goroutine and pass the response to f.
    c := make(chan error, 1)
    req = req.WithContext(ctx)
    go func() { c <- f(http.DefaultClient.Do(req)) }()
    select {
    case <-ctx.Done():
        <-c // Wait for f to return.
        return ctx.Err()
    case err := <-c:
        return err
    }
}

可以看到,在处理超时或者取消这部分都是通过select语句来检测ctx.Done()返回的channel,如果ctx先取消的话,则可以检测上下文取消的原因,然后返回结果。我们还可以接着用一个示例来具体描述它们的行为:

示例:具体示例说明取消

我们这个程序发送一个ping请求到baidu.com,并设置20ms的超时。

 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
34
35
36
37
38
39
package main

import (
	"context"
	"fmt"
	"log"
	"time"

	probing "github.com/prometheus-community/pro-bing"
)

func main() {
	pinger, err := probing.NewPinger("baidu.com")
	if err != nil {
		log.Fatalln(err)
	}

	pinger.Count = 1

	pinger.OnRecv = func(pkt *probing.Packet) {
		fmt.Printf("%d bytes from %s: icmp_seq=%d time=%v\n",
			pkt.Nbytes, pkt.IPAddr, pkt.Seq, pkt.Rtt)
	}

	pinger.OnFinish = func(stats *probing.Statistics) {
		fmt.Printf("\n--- %s ping statistics ---\n", stats.Addr)
		fmt.Printf("%d packets transmitted, %d packets received, %v%% packet loss\n",
			stats.PacketsSent, stats.PacketsRecv, stats.PacketLoss)
		fmt.Printf("round-trip min/avg/max/stddev = %v/%v/%v/%v\n",
			stats.MinRtt, stats.AvgRtt, stats.MaxRtt, stats.StdDevRtt)
	}

	ctx, _ := context.WithTimeout(context.Background(), time.Millisecond * 20)

	err = pinger.RunWithContext(ctx) // block until finished
	if err != nil {
		log.Fatalln(err)
	}
}

由于大部分的情况下,这个延时都会超过20ms,于是我们大概率会得到下面的运行结果:

/posts/language/golang/context/ping_err.png
运行超时的结果

但是,于此同时,我们通过tcpdump监听的结果可以发现,实际上是收到了ping的结果:

/posts/language/golang/context/tcpdump_ping_result.png
tcpdump的结果

内部实现

数据结构

内部定义了一个context.Context的接口,该接口定义了4个方法。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
type Context interface {
	// Deadline 返回 context.Context 被取消的时间,也就是完成工作的截止日期
	// 若没有设置deadline,则ok == false
	Deadline() (deadline time.Time, ok bool)

	// Done 返回一个 Channel,这个Channel会在当前工作完成或者上下文被取消后关闭,
	// 多次调用Done方法会返回同一个Channel;
	// 当然,在该上下文永远不可能被取消的情况下,它也可能返回nil.
	Done() <-chan struct{}

	// Err 返回context.Context结束的原因,它只会在Done方法对应的Channel关闭时返回非空的值
	Err() error

	// Value 从context.Context中获取键对应的值,对于同一个上下文来说,
	// 多次调用Value并传入相同的Key会返回相同的结果,该方法可以用来传递请求特定的数据
	Value(key any) any
}

取消信号

context.WithCancel函数能从context.Context中衍生出一个新的子上下文并返回用于取消该上下文的函数。一旦我们执行返回的取消函数,当前上下文以及它的子上下文都会被取消,所有的Goroutine都会同步收到这一取消信号。

对应的源码如下:

1
2
3
4
5
func WithCancel(parent Context) (ctx Context, cancel CancelFunc) {
	c := newCancelCtx(parent)  // 将传入的上下文包装成私有结构体: context.cancelCtx
	propagateCancel(parent, &c)  // 构建父子上下文之间的关联,当父上下文被取消时,子上下文也会被取消
	return &c, func() { c.cancel(true, Canceled) }
}

定时取消

除了context.WithCancel 之外,context包中的另外两个函数context.WithDeadlinecontext.WithTimeout也都能创建可以被取消的计时器上下文context.timerCtx

1
2
3
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc) {
	return WithDeadline(parent, time.Now().Add(timeout))
}

常用方法

With系列函数

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
// 传递一个父Context作为参数,返回子Context,以及一个取消函数用来取消Context。
func WithCancel(parent Context) (ctx Context, cancel CancelFunc)

// 和WithCancel差不多,它会多传递一个截止时间参数,意味着到了这个时间点,会自动取消Context,
// 当然我们也可以不等到这个时候,可以提前通过取消函数进行取消。
func WithDeadline(parent Context, deadline time.Time) (Context, CancelFunc)

// WithTimeout和WithDeadline基本上一样,这个表示是超时自动取消,是多少时间后自动取消Context的意思
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc)

// WithValue函数和取消Context无关,它是为了生成一个绑定了一个键值对数据的Context,
// 绑定的数据可以通过Context.Value方法访问到,这是我们实际用经常要用到的技巧,一般我们想要通过上下文来传递数据时,可以通过这个方法,
// 如我们需要tarce追踪系统调用栈的时候。
func WithValue(parent Context, key, val interface{}) Context

参考文献

  1. Go语言设计与实现——上下文context
  2. 详解golang中的context
  3. Go Concurrency Patterns: Context