您现在的位置是:首页 >技术教程 >go-Context详解网站首页技术教程

go-Context详解

daliucheng 2024-09-11 12:01:04
简介go-Context详解

Context详解

简介

官网

  1. context go package

  2. context-blog

Context是一个很特殊的接口,在go里面主要承担的责任是在边界(方法,线程等)传递上下文,这些上下文包括

  1. 取消信号
  2. 超时时间
  3. 特殊的参数

需要有几个注意点

  1. 不要传递nil的Context
  2. 不要在一个结构体中存储Context,而是要将它作为方法的参数传递过去,建议放在入参的第一个位置。
  3. 同一个Context可以传递给不同的go goroutines,Context是线程安全的。
  4. 不要将Context作为一个啥都能放的大而全的容器,以至于将函数的参数都放在里面。

出现背景

在开发中面临几个问题

  1. 规定一次操作的超时时间,如果操作超时,操作中止。
  2. 取消这次操作
  3. 此次操作中需要传递一些给下面操作的一些共有的参数,比如 用户标识。

我们以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。例子如下:

这里需要注意下面几点

  1. context是要组成树状结构的。
  2. 子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)
}

运行结果如下:

在这里插入图片描述

从代码可以反推有几种情况验证一下:

  1. 父ctx被取消,然后在用父ctx创建子ctx,子ctx会怎么样?
  2. 父ctx不是cancelCtx,子ctx会怎么样?

代码如下

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

在这里插入图片描述

原则是父影响子,子不影响父

  1. 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的原则

  1. 父context 完成,子context也需要完成(done)
  2. 子context完成,父context不会受到影响
  3. 需要有取消的操作,有两种方式,手动和自动,手动的前提是此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的超时有几种情况需要验证:

  1. 父 > 子
  2. 子 < 父
  3. 子 < 当前时间

验证代码如下:

  1. 在这里插入图片描述

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

  2. 在这里插入图片描述

  3. 在这里插入图片描述

还有一点需要补充:

在这里插入图片描述

WithTimeout

提供了超时时间的能力。调用的是WithDeadline,这里就不在解释了

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

Context到这里就结束了。

风语者!平时喜欢研究各种技术,目前在从事后端开发工作,热爱生活、热爱工作。