go-Context详解

news2024/11/19 18:00:42

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到这里就结束了。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/633128.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Tcl常用语法备忘录-字符串篇

TCL语言中的string命令用于对字符串进行操作&#xff0c;常用的有以下几种用法&#xff1a; string length 语法&#xff1a;string length string 参数说明&#xff1a;string为要计算长度的字符串。 示例&#xff1a; set str "Hello TCL" puts [string lengt…

盘点一个Jupyter显示的细节问题

点击上方“Python爬虫与数据挖掘”&#xff0c;进行关注 回复“书籍”即可获赠Python从入门到进阶共10本电子书 今 日 鸡 汤 弦弦掩抑声声思&#xff0c;似诉平生不得志。 大家好&#xff0c;我是皮皮。 一、前言 前几天在Python白银群【小王子】问了一个Python基础的问题&…

北京通信展的精华内容,都在这里!(下篇)

█ 中国卫通 看上去像一笼汤圆&#xff0c;其实都是天线振子&#xff1a; 紧密围绕刚发射的中星26&#xff1a; 中国卫通的高通量卫星覆盖计划&#xff1a; 针对航空的卫星通信场景&#xff1a; 针对海运的场景&#xff1a; █ 中国电科 中国信科的东西&#xff0c;很多都是应急…

Python潮流周刊#6:Python 3.12 有我贡献的代码!

△点击上方“Python猫”关注 &#xff0c;回复“1”领取电子书 你好&#xff0c;我是猫哥。这里记录每周值得分享的 Python 及通用技术内容&#xff0c;部分为英文&#xff0c;已在小标题注明。&#xff08;标题取自其中一则分享&#xff0c;不代表全部内容都是该主题&#xff…

这是铁哥们

晚上在看读者给我的留言&#xff0c;有一篇文章里面挂了 300 多条留言&#xff0c;因为微信公众号一篇文章只能放出 100 条留言&#xff0c;剩余的就只能存在后台了。 然后我看到有读者的留言还被扔到了垃圾箱。扔垃圾箱这个事情应该是微信的一个默认设置&#xff0c;但是这样励…

【夜深人静算法介绍 | 第一篇】KMP算法

目录 前言&#xff1a; KMP算法简介&#xff1a; 引入概念&#xff1a; 前缀后缀 前缀表&#xff1a; 简单例子&#xff1a; 暴力遍历&#xff1a; KMP算法&#xff1a;​ KMP算法难点&#xff1a; 总结&#xff1a; 前言&#xff1a; 本篇我们将详细的从理论层面介绍一…

Java库Lombok常用注解使用

Lombok已经是很多Java项目最常用的库之一了&#xff0c;我也一直在用&#xff0c;但是仅限于Data、XxxConstructer、Slf4j之类的注解&#xff0c;没有看过其它的注解。 直到前段时间看到别人的代码&#xff0c;使用了一个SneakyThrows注解&#xff0c;搜索了一下&#xff0c;才…

华为OD机试真题 JavaScript 实现【数字涂色】【2022Q4 100分】,附详细解题思路

一、题目描述 疫情过后&#xff0c;希望小学终于又重新开学了&#xff0c;三年二班开学第一天的任务是将后面的黑板报重新制作。 黑板上已经写上了N个正整数&#xff0c;同学们需要给这每个数分别上一种颜色。 为了让黑板报既美观又有学习意义&#xff0c;老师要求同种颜色的…

粘包和半包的解决

粘包产生 public class HelloWordServer {static final Logger log LoggerFactory.getLogger(HelloWordServer.class);public static void main(String[] args) {NioEventLoopGroup boss new NioEventLoopGroup(1);NioEventLoopGroup worker new NioEventLoopGroup();try {…

Java实现微信公众号直接发送参数二维码给用户

文章目录 前言一、参数二维码的作用二、功能实现1. 生成带参数二维码2. 上传二维码图片3. 发送带参数二维码给用户 总结 前言 公众号开发近些年是一个比较热门的方向&#xff0c;今天为大家讲解的是用Java如何实现自动生成二维码图片&#xff0c;有如何把这个和用户信息单独绑…

Python3数据分析与挖掘建模(12)复合分析-相关分析与实现示例

1. 相关分析 1.1 概述 相关分析是一种统计分析方法&#xff0c;用于研究两个或多个变量之间的关系和相互影响程度。它帮助我们了解变量之间的线性关系、趋势和相关程度。 在相关分析中&#xff0c;常用的指标是相关系数&#xff0c;用于衡量两个变量之间的相关程度。最常见的…

linux 定时任务

可以用非root用户创建定时任务 Linux crontab 是用来定期执行程序的命令。 当安装完成操作系统之后&#xff0c;默认便会启动此任务调度命令。 crond 命令每分钟会定期检查是否有要执行的工作&#xff0c;如果有要执行的工作便会自动执行该工作。 注意&#xff1a;新创建的 cro…

怎么查询电脑的登录记录及密码更改情况?

源头是办公室公用的电脑莫名其妙打不开了&#xff0c;问别人也都不知道密码是多少 因为本来就没设密码啊&#xff01;&#xff08;躺倒&#xff09; 甚至已经想好了如果是50万想攻破电脑&#xff0c;被po抓住要怎么花这笔钱了 是我想太多 当然最后也没解决&#xff0c;莫名…

27 getcwd 的调试

前言 同样是一个 很常用的 glibc 库函数 不管是 用户业务代码 还是 很多类库的代码, 基本上都会用到 获取当前路径 不过 我们这里是从 具体的实现 来看一下 测试用例 就是简单的使用了一下 getcwd rootubuntu:~/Desktop/linux/HelloWorld# cat Test04Getcwd.c #inc…

11.DIY可视化-拖拽设计1天搞定主流小程序-小程序首页公告详情页面

小程序首页公告详情页面 本教程均在第一节中项目启动下操作 小程序首页公告详情页面前言一、添加界面,布局1.设定组件样式:数据绑定 二. 新增接口三:绑定公告四.查看效果五.动态参数设置 :之前是指定了公告单条数据2.优化还在那时详情页<p>标签:借助工具查看,清空绑定修改…

【树莓派】树莓派4B镜像安装(使用Raspberry Pi image)

本文主要记录下如何使用Raspberry Pi image 软件进行树莓派镜像进行安装。 官网&#xff1a;Raspberry Pi OS – Raspberry Pi 百度网盘&#xff1a; 链接&#xff1a;https://pan.baidu.com/s/1G7z1Fdvk5Chmhj894WPU3A 提取码&#xff1a;xnzw 一、格式化SD卡 若SD卡存在…

【Linux】按键驱动程序

【Linux】按键驱动程序 前言&#xff1a; 一、按键驱动程序的背景知识 1.1 查询方式 1.2 休眠-唤醒方式 1.3 poll方式 1.4 异步通知 1.5 总结 二、按键驱动程序的框架 三、按键驱动程序实战 3.1 头文件&#xff08;button_drv.h&#xff09; 3.2 驱动程序&#xf…

我的开源实践之路!这一路我遇到的困难和收获总结

Datawhale干货 作者&#xff1a;诸葛子房&#xff0c;Datawhale成员 从参与Apache开源项目&#xff0c;到凭借业务需求独自开发个人开源项目&#xff1b;从项目开源出来无人问津到至今500star&#xff0c;多个企业级用户&#xff0c;在开源过程中&#xff0c;我也从走过低谷&a…

Matplotlib的一些总结

plt.figure(numNone, figsizeNone, dpiNone, facecolorNone, edgecolorNone, frameonTrue) 参数说明&#xff1a; 1.num&#xff1a;图像编码或者名称&#xff0c;数字是编码&#xff0c;字符串是名称 2.figsize&#xff1a;宽和高&#xff0c;单位是英尺 3.dpi&#xff1a;指…

chatgpt赋能python:Python怎么取二进制低三位?

Python怎么取二进制低三位&#xff1f; 在Python编程中&#xff0c;处理位运算是一个非常常见的任务。其中&#xff0c;取二进制低三位也是其中的一项操作。那么&#xff0c;如何实现这个操作呢&#xff1f;本篇文章将为大家介绍Python如何取二进制低三位的方法。 什么是二进…