MelonBlog

go语言Context的用法

在写web项目的时候经常需要知道一段逻辑的上下文信息,比如一个调用联里,很多地方都需要知道当前用户的信息,如果每个函数都传递一个用户参数,显得非常不优雅,而且会导致函数的参数过多,可读性非常差。

在java中,可以使用ThreadLocal来存储整个调用链(单机单线程)需要共享的数据,在go语言中,Context承担了类似ThreadLocal的角色,并且

Context还能在多个Goroutine中共享(ThreadLocal不能跨线程使用)
取消上下文,并提醒其他函数(避免不必要的资源消耗)

这两种能力是ThreadLocal不具备的。

Context

创建Context

创建Context的两种方法:

context.TODO()
context.Background()

这两种方式实际上效果都一样,但是有个约定俗成的规范,就是如果你不知道这个Context的用途的情况下,使用context.Background(),反之则用context.TODO()。

还有一个约定就是,Context一般都做函数的第一个参数。

Context存储信息

context包有一个withValue函数用于存储信息到Context里

# parent 要存储信息的ctx
# key 用于检索信息的key
# value 需要存放在上下文里的信息
func WithValue(parent Context, key, val any) Context

例子🌰:

func doSomething(ctx context.Context) {
	fmt.Println("Doing something!")
	fmt.Printf("%s\n", ctx.Value("key"))
}
func main() {
	ctx := context.Background()
	ctx = context.WithValue(ctx, "key", "value")
	doSomething(ctx)
}

Context嵌套和包装

Context中所有存储的值都是immutable值,是不可以修改的,因为context.WithValue函数会返回一个新的Context,新的Context内部包含老的Context和新设置的值。所以每次对Context操作,无论是新增还是修改key,都是针对Context做了一层包装。老的value并不会丢失,可以通过老ctx变量检索到。

是不是有点类似docker的layer?


例子🌰:

func doSomething2(ctx context.Context) {
	fmt.Printf("do something2 -- ctx-key: %s \n", ctx.Value("key"))
}
func doSomething1(ctx context.Context) {
	fmt.Printf("do something1 -- ctx-key: %s \n", ctx.Value("key"))
	newCtx := context.WithValue(ctx, "key", "value2")
	doSomething2(newCtx)
	fmt.Printf("do something1 -- ctx-key: %s \n", ctx.Value("key"))
}
func main() {
	ctx := context.Background()
	ctx = context.WithValue(ctx, "key", "value")
	doSomething1(ctx)
}

Context的取消和通知机制

Context的Done()函数会返回一个channel,当Context取消的时候会关闭这个channel,select这个channel就可以在Context取消的时候收到通知。


Context有3种方式可以触发这个通知:

func WithCancel(parent Context) (ctx Context, cancel CancelFunc)
func WithDeadline(parent Context, d time.Time) (Context, CancelFunc)
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc)


最常用的就是使用WithCancel函数来创建一个可以取消的Context和一个取消函数(CancelFunc),调用这个取消函数就可以取消这个上下文。


例子🌰:

func doSomething2(ctx context.Context) {
	fmt.Println("do something2")
	for {
		select {
		case <-ctx.Done():
			fmt.Println("ctx done")
			return
		default:
		}
	}
}
func doSomething1(ctx context.Context) {
	fmt.Println("do something1")
	ctx, cancel := context.WithCancel(ctx)
	go doSomething2(ctx)
	time.Sleep(3 * time.Second)
	cancel()
}
func main() {
	ctx := context.Background()
	doSomething1(ctx)
}


和WithCancel类似,WithDeadline和WithTimeout函数也可以触发这个通知,但是触发方式除了主动调用CancelFunc,还可以根据传入的时间参数自动触发。