您现在的位置是:首页 >技术教程 >go-Context详解网站首页技术教程
go-Context详解
Context详解
简介
官网
Context是一个很特殊的接口,在go里面主要承担的责任是在边界(方法,线程等)传递上下文,这些上下文包括
- 取消信号
 - 超时时间
 - 特殊的参数
 
需要有几个注意点
- 不要传递nil的Context
 - 不要在一个结构体中存储Context,而是要将它作为方法的参数传递过去,建议放在入参的第一个位置。
 - 同一个Context可以传递给不同的go goroutines,Context是线程安全的。
 - 不要将Context作为一个啥都能放的大而全的容器,以至于将函数的参数都放在里面。
 
出现背景
在开发中面临几个问题
- 规定一次操作的超时时间,如果操作超时,操作中止。
 - 取消这次操作
 - 此次操作中需要传递一些给下面操作的一些共有的参数,比如 用户标识。
 
我们以web开发为背景举个例子:
一次web请求包含redis操作,数据库操作,RPC操作。并且每一个请求都会有自己的go goroutine,需要在这个go goroutine中设置一些用户的信息,以便后续操作需要用到(比如 链路追踪,rpc调用中的用户信息打点参数等),当请求超时或者取消的时候,后续已经触发的操作能立即取消,并且释放相应的资源。(数据库取消查询,rpc取消调用)
接口
Context是一个接口。
// A Context carries a deadline, cancellation signal, and request-scoped values
// across API boundaries. Its methods are safe for simultaneous use by multiple
// goroutines.
type Context interface {
	// 返回一个channel,表示此Contenxt已经关闭
    Done() <-chan struct{}
 	//表示channel取消或者关闭的原因
    Err() error
	// Context的超时时间,如果设置的话,ok返回为true,没有设置就是false
    Deadline() (deadline time.Time, ok bool)
    // 从Context通过key返回存储的Value,没有就是nil
    Value(key interface{}) interface{}
}
 
方法分析
Context包中提供了下面的几个可导出的方法,这些方法已经实现了上面所说的功能,可以看到,他们必须要传递一个parent context(其实就是基本的Context),并且可以互相嵌套,从而生成一个树状结构的Context。例子如下:
这里需要注意下面几点
- context是要组成树状结构的。
 - 子context在父类的基础上包装增加功能而来。
 
func main() {
	ctx := context.Background()
	ctx = context.WithValue(ctx, "k1", "v1")
	ctxCancel, cancelFunc := context.WithCancel(ctx)
	cancelFunc()
	timeoutCtx, c := context.WithTimeout(ctxCancel, time.Hour
}
 

下面介绍使用方式和基本原理
withValue
package main
import (
	"context"
	"fmt"
)
type User struct {
	id int64
	name string
}
func main() {
	user := User{
		id:   1,
		name: "小明",
	}
	// 先构建一个基本的Context
	ctx := context.Background()
	// 用 WithValue 来包装ctx,将user存放在Context中,key为 user
	ctx = context.WithValue(ctx, "user", user)
	CheckNumberIsValid(ctx,"15909089432")
}
func CheckNumberIsValid(ctx context.Context,number string) (bool,error)  {
	user := ctx.Value("user").(User)
	fmt.Printf("%v",user)
	return true,nil
}
运行结果如下:
{1 小明}
 
WithValue方法如下

需要注意value方法中的 for循环,要知道context是嵌套的,一个context只能存放一对值,要想继续存放必须context嵌套处理,代码如下:

WithCancel
package main
import (
	"golang.org/x/net/context"
	"log"
	"time"
)
func main() {
	ctx := context.Background()
	ctx, cancelFunc := context.WithCancel(ctx)
	go func() {
		log.Println("wait")
		select {
		case <-ctx.Done():
			log.Println("done" + ctx.Err().Error())
		}
	}()
	time.Sleep(1 * time.Second)
	log.Println("ctx done ")
	cancelFunc()
	time.Sleep(time.Hour)
}
 
WithCancel创建一个可取消的context,如上面所示,goroutine监听ctx done的channel,一秒之后调用取消函数,打印取消原因,调用结果如下:

源码分析如下
func WithCancel(parent Context) (ctx Context, cancel CancelFunc) {
	if parent == nil {
		panic("cannot create context from nil parent")
	}
    // 创建cancelCtx,cancelCtx是一个不可导出的,并且实现了Context接口
	c := newCancelCtx(parent)
    // 传播取消操作
	propagateCancel(parent, &c)
    // 返回创建的cancelCtx,返回取消函数
	return &c, func() { c.cancel(true, Canceled) }
}
// 入参为父Contex,和子Context
func propagateCancel(parent Context, child canceler) {
    // 父ctx不能取消直接返回
	done := parent.Done()
	if done == nil {
		return // parent is never canceled
	}
	select {
	case <-done:
	 // 可以从父ctx中获取到信号,说明父context以及取消,此种情况下,子context也应该被取消掉
		child.cancel(false, parent.Err())
		return
	default:
	}
    
	//判断ctx的的类型
	if p, ok := parentCancelCtx(parent); ok { 
		p.mu.Lock()
		if p.err != nil {
			// 父ctx取消了,子ctx也应该取消掉
			child.cancel(false, p.err)
		} else {
            // 将子ctx添加到父ctx
			if p.children == nil {
				p.children = make(map[canceler]struct{})
			}
			p.children[child] = struct{}{}
		}
		p.mu.Unlock()
	} else {
        // 如果父类型不是cancelCtx,就需要启动goroutine
		atomic.AddInt32(&goroutines, +1)
		go func() {
			select {
			case <-parent.Done():
                // 等待父ctx关闭,取消子ctx
				child.cancel(false, parent.Err())
			case <-child.Done():
                // 子ctx关闭
			}
		}()
	}
}
func parentCancelCtx(parent Context) (*cancelCtx, bool) {
	done := parent.Done()
    // 父ctx是否关闭
	if done == closedchan || done == nil {
		return nil, false
	}
    // 看是否是cancelCtx
	p, ok := parent.Value(&cancelCtxKey).(*cancelCtx)
	if !ok {
		return nil, false
	}
    // 父ctx是cancelCtx,加载done channel,判断是否关闭
	pdone, _ := p.done.Load().(chan struct{})
	if pdone != done {
		return nil, false
	}
	return p, true
}
//cancel是canceler接口的方法,此接口表示 可直接取消。
// removeFromParent :是否从父context中移除
// err 错误原因
func (c *cancelCtx) cancel(removeFromParent bool, err error) {
	if err == nil {
		panic("context: internal error: missing cancel error")
	}
	c.mu.Lock()
    // 此ctx已经被取消掉
	if c.err != nil {
		c.mu.Unlock()
		return 
	}
	c.err = err
    // 得到done channel,Done的channel是懒加载
	d, _ := c.done.Load().(chan struct{})
	if d == nil {
		c.done.Store(closedchan)
	} else {
		close(d)
	}
	for child := range c.children {
		// 会依次关闭子ctx
		child.cancel(false, err)
	}
	c.children = nil
	c.mu.Unlock()
	// 从父ctx中移除此ctx
	if removeFromParent {
		removeChild(c.Context, c)
	}
}
// canceler接口表示可以直接取消,
// 只有两个ctx实现了,
// 1: cancelCtx 
// 2: timerCtx
type canceler interface {
	cancel(removeFromParent bool, err error)
	Done() <-chan struct{}
}
// 实现Context接口,可提供多个子ctx,
type cancelCtx struct {
	Context
	mu       sync.Mutex            // protects following fields
	done     atomic.Value          // of chan struct{}, created lazily, closed by first cancel call
	children map[canceler]struct{} // set to nil by the first cancel call
	err      error                 // set to non-nil by the first cancel call
}
func (c *cancelCtx) Value(key any) any {
	if key == &cancelCtxKey {
		return c
	}
	return value(c.Context, key)
}
// 懒加载,只有在调用Done方法的时候才会赖加载
func (c *cancelCtx) Done() <-chan struct{} {
	d := c.done.Load()
	if d != nil {
		return d.(chan struct{})
	}
	c.mu.Lock()
	defer c.mu.Unlock() 
	d = c.done.Load()
	if d == nil {
		d = make(chan struct{}) // 关闭一个channel,还可以从channel中可读取
		c.done.Store(d)
	}
	return d.(chan struct{})
}
func (c *cancelCtx) Err() error {
	c.mu.Lock()
	err := c.err
	c.mu.Unlock()
	return err
}
 
从代码可以看出,cancelCtx提供了取消的能力,并且子ctx取消不会影响到父ctx,父ctx取消,子ctx会取消。

代码如下
package main
import (
	"context"
	"log"
	"time"
)
func main() {
	ctx := context.Background()
	ctx1 := context.WithValue(ctx, "k1", "v1")
	ctx11, cancelFunc11 := context.WithCancel(ctx)
	ctx2, _ := context.WithCancel(ctx1)
	ctx22 := context.WithValue(ctx11, "k2", "v2")
	
	go func() {
		log.Println("ctx22 wait")
		select {
		case <-ctx22.Done():
			log.Println("ctx22 done,",ctx22.Err())
		}
	}()
	go func() {
		log.Println("ctx2 wait")
		select {
		case <-ctx2.Done():
			log.Println("ctx2 done,",ctx2.Err())
		}
	}()
	time.Sleep(time.Second)
	log.Println("call cancelFunc11")
	cancelFunc11()
	time.Sleep(time.Hour)
}
 
运行结果如下:

从代码可以反推有几种情况验证一下:
- 父ctx被取消,然后在用父ctx创建子ctx,子ctx会怎么样?
 - 父ctx不是cancelCtx,子ctx会怎么样?
 
代码如下
-  
package main import ( "context" "log" "time" ) func main() { ctx := context.Background() ctx, cancelFunc := context.WithCancel(ctx) // 父ctx取消 cancelFunc() ctx1, cancelFunc1 := context.WithCancel(ctx) go func() { log.Println("ctx1 wait") select { case <-ctx1.Done(): log.Println("ctx1 done,",ctx1.Err()) } }() time.Sleep(time.Second) log.Println("call cancelFunc1") // 调用子ctx cancelFunc1() time.Sleep(time.Hour) } 

原则是父影响子,子不影响父
-  
package main import ( "context" "errors" "log" "sync/atomic" "time" ) type MyContext struct { done atomic.Value // of chan struct{}, created lazily, closed by first cancel call err error } func (m *MyContext) Deadline() (deadline time.Time, ok bool) { return time.Time{}, false } func (m *MyContext) Done() <-chan struct{} { doneChannel := make(chan struct{}) m.done.Store(doneChannel) return doneChannel } func (m *MyContext) Err() error { return m.err } func (m *MyContext) Value(key any) any { return nil } func main() { // 创建自定义context ctx := &MyContext{} // 创建cancel ctx ctx1, _ := context.WithCancel(ctx) // 等待ctx2取消 go func() { log.Println("ctx2 wait") select { case <-ctx1.Done(): log.Println("ctx2 done,",ctx1.Err()) } }() // 过一秒 父ctx 关闭 time.Sleep(time.Second) // 这是我自己写的演示,不太规范,但能说明问题 log.Println("parent context done") ctx.err = errors.New("my context done close") c := ctx.done.Load().(chan struct{}) close(c) time.Sleep(time.Hour) }结果如下:
 

结果很显然,和上面一样,父context被取消,子context也得被取消。回过头在来看看源码中逻辑:

Context的基本原则
到这里,体会一下context的原则
- 父context 完成,子context也需要完成(done)
 - 子context完成,父context不会受到影响
 - 需要有取消的操作,有两种方式,手动和自动,手动的前提是此context实现了cancel接口,自动的话,需要启动一个goroutine,监听父Context的Done信号,从而取消。
 
带着上面的原则,继续往下看剩余的方法。
WithDeadline
context提供了超时时间的能力。代码如下
package main
import (
	"context"
	"log"
	"time"
)
func main() {
	ctx := context.Background()
    // 截至时间是两秒之后
	ctx, cancelFunc := context.WithDeadline(ctx, time.Now().Add(time.Second*2))
	go func() {
		log.Println("ctx wait")
		select {
		case <-ctx.Done():
			log.Println("ctx done,",ctx.Err())
		}
	}()
	// 五秒之后调用取消函数
	time.Sleep(time.Second * 5)
	log.Println("ctx done")
	cancelFunc()
	time.Sleep(time.Hour)
}
 
结果如下:

源码分析如下:
func WithDeadline(parent Context, d time.Time) (Context, CancelFunc) {
	if parent == nil {
		panic("cannot create context from nil parent")
	}
    // parent有超时时间,并且比子ctx的超时时间小,也就是父context的deadline比子deadline小。
    // 直接返回了cancelCtx,本质来说,还是以父为主
	if cur, ok := parent.Deadline(); ok && cur.Before(d) {
		// The current deadline is already sooner than the new one.
		return WithCancel(parent)
	}
    // 从这往下走的前提是父context有deadline并且deadline比子的deadline大
    // 或者父context没有deadline能力
    
    // 创建timerCtx
	c := &timerCtx{
		cancelCtx: newCancelCtx(parent),
		deadline:  d,
	}
    // 传播cancel,
	propagateCancel(parent, c)
    // 算当前时间的差值
	dur := time.Until(d)
	if dur <= 0 {
        // 比当前时间小
        // 取消自己,并且从父context中移除自己
		c.cancel(true, DeadlineExceeded) 
		return c, func() { c.cancel(false, Canceled) }
	}
	c.mu.Lock()
	defer c.mu.Unlock()
    //创建一个timer,定时取消
	if c.err == nil {
		c.timer = time.AfterFunc(dur, func() {
			c.cancel(true, DeadlineExceeded)
		})
	}
	return c, func() { c.cancel(true, Canceled) }
}
// timerCtx实现了cancel接口,并且聚合了cancelCtx,
// 在cancelCtx的基础上增加了定时取消的能力(timer)
type timerCtx struct {
	cancelCtx
	timer *time.Timer // Under cancelCtx.mu.
	deadline time.Time
}
func (c *timerCtx) Deadline() (deadline time.Time, ok bool) {
	return c.deadline, true
}
func (c *timerCtx) String() string {
	return contextName(c.cancelCtx.Context) + ".WithDeadline(" +
		c.deadline.String() + " [" +
		time.Until(c.deadline).String() + "])"
}
func (c *timerCtx) cancel(removeFromParent bool, err error) {
    // 调用cancel取消
	c.cancelCtx.cancel(false, err)
	if removeFromParent {
		// Remove this timerCtx from its parent cancelCtx's children.
		removeChild(c.cancelCtx.Context, c)
	}
	c.mu.Lock()
	if c.timer != nil {
		c.timer.Stop()
		c.timer = nil
	}
	c.mu.Unlock()
}
 
从源码可以看到,WithDeadline的底层实现是timerCtx
timerCtx聚合了cancelCtx,有取消的能力,并且通过timer增加了超时自动取消的能力,它把上面说的手动取消和自动取消结合在一块了。
timerCtx有超时时间的功能,为了上面所说的原则,需要在创建的时候通过超时时间来判断父context和子context是否已经完成。
从代码可以推测父子deadline的超时有几种情况需要验证:
- 父 > 子
 - 子 < 父
 - 子 < 当前时间
 
验证代码如下:
-  

父是父,子是子,分开的。
 -  

 -  

 
还有一点需要补充:

WithTimeout
提供了超时时间的能力。调用的是WithDeadline,这里就不在解释了
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc) {
	return WithDeadline(parent, time.Now().Add(timeout))
}
 
Context到这里就结束了。
            




U8W/U8W-Mini使用与常见问题解决
QT多线程的5种用法,通过使用线程解决UI主界面的耗时操作代码,防止界面卡死。...
stm32使用HAL库配置串口中断收发数据(保姆级教程)
分享几个国内免费的ChatGPT镜像网址(亲测有效)
Allegro16.6差分等长设置及走线总结