深入理解Go——context(2)

2023-10-27

结构体

emptyCtx

源码中定义了 Context 接口后,并且给出了一个实现:

type emptyCtx int

func (*emptyCtx) Deadline() (deadline time.Time, ok bool) {
    return
}

func (*emptyCtx) Done() <-chan struct{} {
    return nil
}

func (*emptyCtx) Err() error {
    return nil
}

func (*emptyCtx) Value(key interface{}) interface{} {
    return nil
}

可以看出,empty实际上什么也没有实现,所有方法基本都返回空,或者直接返回。
emptyCtx被包装成:

var (
    background = new(emptyCtx)
    todo       = new(emptyCtx)
)

通过下面两个导出的函数(首字母大写)对外公开:

func Background() Context {
    return background
}

func TODO() Context {
    return todo
}

通过上一节Context(1)提到过Background()它通常用在 main 函数中,作为所有 context 的根节点。

而对于TODO(),通常用在并不知道传递什么 context的情形。例如,调用一个需要传递 context 参数的函数,你手头并没有其他 context 可以传递,这时就可以传递 todo。这常常发生在重构进行中,给一些函数添加了一个 Context 参数,但不知道要传什么,就用 todo “占个位子”,最终要换成其他 context。

cancleCtx

cancleCtx是一个可以取消的 Context,实现了 canceler 接口。它直接将接口 Context 作为它的一个匿名字段,这样,它就可以被看成一个 Context。

type cancelCtx struct {
    Context

    // 保护之后的字段
    mu       sync.Mutex
    done     chan struct{}
    children map[canceler]struct{}
    err      error
}

先来看 Done() 方法的实现:

func (c *cancelCtx) Done() <-chan struct{} {
    c.mu.Lock()
    if c.done == nil {
        c.done = make(chan struct{})
    }
    d := c.done
    c.mu.Unlock()
    return d
}

c.done 是“懒汉式”创建,只有调用了 Done() 方法的时候才会被创建。
再次说明,函数返回的是一个只读的 channel,而且没有地方向这个 channel 里面写数据。所以,直接调用读这个 channel,协程会被 block 住。一般通过搭配 select 来使用。一旦关闭,就会立即读出零值。

接下来,我们重点关注 cancel() 方法的实现:

func (c *cancelCtx) cancel(removeFromParent bool, err error) {
    // 必须要传 err
    if err == nil {
        panic("context: internal error: missing cancel error")
    }
    c.mu.Lock()
    if c.err != nil {
        c.mu.Unlock()
        return // 已经被其他协程取消
    }
    // 给 err 字段赋值
    c.err = err
    // 关闭 channel,通知其他协程
    if c.done == nil {
        c.done = closedchan
    } else {
        close(c.done)
    }

    // 遍历它的所有子节点
    for child := range c.children {
        // 递归地取消所有子节点
        child.cancel(false, err)
    }
    // 将子节点置空
    c.children = nil
    c.mu.Unlock()

    if removeFromParent {
        // 从父节点中移除自己 
        removeChild(c.Context, c)
    }
}

总体来看,cancel() 方法的功能就是

  • 关闭 channel:c.done
  • 递归地取消它的所有子节点;
  • 从父节点从删除自己。

达到的效果是通过关闭 channel,将取消信号传递给了它的所有子节点。goroutine 接收到取消信号的方式就是 select 语句中的读 c.done 被选中。

对于一个关闭的channel,如果继续向channel发送数据,会引起panic。
如果继续读数据,得到的是零值(对于int,就是0)。
所以可以利用只读的channle来达到阻塞的目的,当channle关闭时,就可以读出0值了。

我们再来看创建一个可取消的 Context 的方法:

// Canceled is the error returned by Context.Err when the context is canceled.
var Canceled = errors.New("context canceled")

func WithCancel(parent Context) (ctx Context, cancel CancelFunc) {
    c := newCancelCtx(parent)
    propagateCancel(parent, &c)
    return &c, func() { c.cancel(true, Canceled) }
}

// newCancelCtx returns an initialized cancelCtx.
func newCancelCtx(parent Context) cancelCtx {
	return cancelCtx{Context: parent}
}

这是一个暴露给用户的方法,在上一节已经使用过。
重点需要关注propagateCancel()方法:


// propagateCancel arranges for child to be canceled when parent is.
func propagateCancel(parent Context, child canceler) {
	done := parent.Done()
	if done == nil {// 父节点是个空节点,emptyCtx
		return // parent is never canceled
	}

	select {
	case <-done:
		// parent is already canceled
		child.cancel(false, parent.Err())
		return
	default:
	}
	
	// 找到可以取消的父 context
	if p, ok := parentCancelCtx(parent); ok {
		p.mu.Lock()
		if p.err != nil {
			// parent has already been canceled
			// 父节点已经被取消了,本节点(子节点)也要取消
			child.cancel(false, p.err)
		} else {// 父节点未取消
			if p.children == nil {
				p.children = make(map[canceler]struct{})
			}
			// "挂到"父节点上
			p.children[child] = struct{}{}
		}
		p.mu.Unlock()
	} else {
		atomic.AddInt32(&goroutines, +1)
		// 如果没有找到可取消的父context。新启动一个协程监控父节点或子节点取消信号
		go func() {
			select {
			case <-parent.Done():
				child.cancel(false, parent.Err())
			case <-child.Done():
			}
		}()
	}
}

timerCtx

timerCtx 基于 cancelCtx,只是多了一个 time.Timer 和一个 deadline。Timer 会在 deadline 到来时,自动取消 context。

type timerCtx struct {
    cancelCtx
    timer *time.Timer // Under cancelCtx.mu.

    deadline time.Time
}

timerCtx 首先是一个 cancelCtx,所以它能取消。看下 cancel() 方法:

func (c *timerCtx) cancel(removeFromParent bool, err error) {
    // 直接调用 cancelCtx 的取消方法
    c.cancelCtx.cancel(false, err)
    if removeFromParent {
        // 从父节点中删除子节点
        removeChild(c.cancelCtx.Context, c)
    }
    c.mu.Lock()
    if c.timer != nil {
        // 关掉定时器,这样,在deadline 到来时,不会再次取消
        c.timer.Stop()
        c.timer = nil
    }
    c.mu.Unlock()
}

创建 timerCtx 的方法:

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

WithTimeout 函数直接调用了 WithDeadline,传入的 deadline 是当前时间加上 timeout 的时间,也就是从现在开始再经过 timeout 时间就算超时。

func WithDeadline(parent Context, deadline time.Time) (Context, CancelFunc) {
    if cur, ok := parent.Deadline(); ok && cur.Before(deadline) {
        // 如果父节点 context 的 deadline 早于指定时间。直接构建一个可取消的 context。
        // 原因是一旦父节点超时,自动调用 cancel 函数,子节点也会随之取消。
        // 所以不用单独处理子节点的计时器时间到了之后,自动调用 cancel 函数
        return WithCancel(parent)
    }

    // 构建 timerCtx
    c := &timerCtx{
        cancelCtx: newCancelCtx(parent),
        deadline:  deadline,
    }
    // 挂靠到父节点上
    propagateCancel(parent, c)

    // 计算当前距离 deadline 的时间
    d := time.Until(deadline)
    if d <= 0 {
        // 直接取消
        c.cancel(true, DeadlineExceeded) // deadline has already passed
        return c, func() { c.cancel(true, Canceled) }
    }
    c.mu.Lock()
    defer c.mu.Unlock()
    if c.err == nil {
        // d 时间后,timer 会自动调用 cancel 函数。自动取消
        c.timer = time.AfterFunc(d, func() {
            c.cancel(true, DeadlineExceeded)
        })
    }
    return c, func() { c.cancel(true, Canceled) }
}

也就是说仍然要把子节点挂靠到父节点,一旦父节点取消了,会把取消信号向下传递到子节点,子节点随之取消。

有一个特殊情况是,如果要创建的这个子节点的 deadline 比父节点要晚,也就是说如果父节点是时间到自动取消,那么一定会取消这个子节点,导致子节点的 deadline 根本不起作用,因为子节点在 deadline 到来之前就已经被父节点取消了。

valueCtx

type valueCtx struct {
    Context
    key, val interface{}
}

它实现了两个方法:

func (c *valueCtx) String() string {
    return fmt.Sprintf("%v.WithValue(%#v, %#v)", c.Context, c.key, c.val)
}

func (c *valueCtx) Value(key interface{}) interface{} {
    if c.key == key {
        return c.val
    }
    return c.Context.Value(key)
}

由于它直接将 Context 作为匿名字段,因此仅管它只实现了 2 个方法,其他方法继承自父 context。但它仍然是一个 Context,这是 Go 语言的一个特点。

创建 valueCtx 的函数:

func WithValue(parent Context, key, val interface{}) Context {
    if key == nil {
        panic("nil key")
    }
    if !reflect.TypeOf(key).Comparable() {
        panic("key is not comparable")
    }
    return &valueCtx{parent, key, val}
}

对 key 的要求是可比较,因为之后需要通过 key 取出 context 中的值,可比较是必须的。
通过层层传递 context,最终形成这样一棵树:

在这里插入图片描述

取值的过程,实际上是一个递归查找的过程,因为查找方向是往上走的,所以,父节点没法获取子节点存储的值,子节点却可以获取父节点的值。

func (c *valueCtx) Value(key interface{}) interface{} {
    if c.key == key {
        return c.val
    }
    return c.Context.Value(key)
}
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

深入理解Go——context(2) 的相关文章

  • 切片文字中的求值顺序

    我最近浏览了Go的 语言规范 https golang org ref spec Order of evaluation https golang org ref spec Order of evaluation但发现评估顺序与本文档中解释
  • Go中的切片分配是否复制内存

    目的 我有一个大缓冲区 我想要一个指向缓冲区中不同位置的指针数组 切片 我在做什么 datPtrs make byte n for i 0 i
  • Golang中的匿名接口实现

    在Go中 有没有办法匿名满足接口 似乎没有 但这是我最好的尝试 In the 操场 http play golang org p 4i 8o087dg package main import fmt type Thing interface
  • 从恐慌中恢复的程序未按预期退出

    根据我的理解 当恐慌恢复时 我期望程序退出并表现出正常行为 但事实并非如此 我期望最后一行打印 程序结束 是正确的吗 如果出现运行时错误 它不会被打印 是吗 package main import fmt func main defer f
  • 如何用 Go 将多个字符串解析为模板?

    有没有像这样的简单方法template ParseFiles base html home html 但是对于字符串来说 如何从一组字符串构建模板 我有一个基本模板和一个页面模板列表 全部为字符串 我想在基本模板之上构建它们 我想出了如何合
  • Go1编译器如何工作?

    我在一个学校项目中接触 Go 大约一个月了 我注意到 src pkg go 文件夹中的 go ast go token go parser 等包 但是 gc 编译器基于位于 src cmd gc 中的 C 文件 我的问题是关于 Go1 中用
  • Go 中的格式错误 - %s %v 或 %w

    s v and w可用于格式化 Go 中的错误 将它们转换为字符串 fmt Errorf 它们在 Go 自己的工具中的使用方式似乎有所不同 In cmd go internal get path go https github com go
  • ConstantTimeByteEq 如何工作?

    在大神的密码库里 找到了这个函数ConstantTimeByteEq http golang org src pkg crypto subtle constant time go s 897 936 L17 它有什么作用 如何工作 Cons
  • 如果覆盖率低于一定百分比,则单元测试失败

    我制作了一个执行的 makefilego test cover 是否有可能失败make unit tests如果覆盖范围低于 X 则命令 我该怎么做呢 您可以使用TestMain在你的测试中做到这一点 TestMain 可以充当测试的自定义
  • IntelliJ 2017.1.2 GOLANG 调试不适用于包中的断点

    我的应用程序由一个 main go 文件和一些包组成 当在 main go 中命中断点时 IntelliJ 按预期工作 显示变量值等 但是 当在不同的包中设置断点时 除了被命中之外 不会显示任何变量 并且不会跳过 进入功能按预期工作 被击中
  • 将绝对路径和相对路径组合起来得到新的绝对路径

    我正在编写一个程序 其中一个组件必须能够采用给定的路径 例如 help index html or help 和基于该位置的相对路径 例如 otherpage index html or sub dir of help or help2 h
  • 具有多个等待组的管道中通道范围内的死锁

    我正在练习通过同时将计算分为 100 组来计算阶乘的挑战 我解决了 WaitGroups 上的很多问题 但仍然处于calculateFactorial函数我在通道部分的范围上陷入了僵局 希望有人能指出这个问题 谢谢 package main
  • 在 Go 中执行字节数组

    我正在尝试在 Go 程序中执行 shellcode 类似于使用其他语言执行此操作的方式 示例 1 C 程序中的 Shellcode https stackoverflow com questions 16626857 shellcode i
  • Ajax 将文件上传到内容类型为 Multipart 的 GoLang 服务器

    我正在尝试使用多部分表单将音频文件上传到 Golang 服务器 然而 Go 返回错误 multipart NextPart bufio buffer full 我相信这表明我的 Javascript 请求中存在不属于多部分格式的内容 这是我
  • 使用 google.protobuf.Timestamp 在 Go 中解析带有时区偏移的日期时间戳

    我正在创建一个将使用 GRPC 和 protobuf 的 Go 应用程序 我的 RPC 服务应获取包含类型的消息google protobuf Timestamp 解析它并最终将其保存在数据库中或对其执行更多操作 我对什么被认为是该类型的有
  • Golang 优雅地关闭 HTTP 服务器并进行错误处理

    我正在让我的 HTTP 服务器正常关闭 我从帖子中获取了提示here https stackoverflow com questions 39320025 how to stop http listenandserve 并且到目前为止已经像
  • 构建链代码时 ltdl.h 未找到错误

    我正在尝试使用构建链码go build 当我运行 Go build 命令时它的报告 hyperledger fabric vendor github com miekg pkcs11 pkcs11 g o 29 18 fatal error
  • 在 Go 中跟踪 HTTP 请求时指定超时

    我知道通过执行以下操作来指定 HTTP 请求超时的常用方法 httpClient http Client Timeout time Duration 5 time Second 但是 我似乎不知道在跟踪 HTTP 请求时如何执行相同的操作
  • 为什么结构中“[0]byte”的位置很重要?

    0 byte在golang中不应该占用任何内存空间 但这两个结构体的大小不同 type bar2 struct A int 0 byte type bar3 struct 0 byte A int 那么为什么这个位置 0 byte这里重要吗
  • 是否可以获取有关 Golang 中调用者函数的信息?

    是否可以获取有关 Golang 中调用者函数的信息 例如 如果我有 func foo Do something func main foo 我怎样才能得到那个foo已被呼叫来自main 我可以用其他语言实现这一点 例如在 C 中我只需要使用

随机推荐

  • Q-learning和Sarsa

    一 Q learning Q Learning的目的是学习特定state下 特定action的价值 是建立一个Q table 以state为行 action为列 通过每个动作带来的奖赏更新Q table 是异策略 行动策略和评估策略不是一个
  • Java实验报告(四)

    文章目录 题目一 题目二 题目三 题目四 题目五 题目六 题目七 题目一 一 程序一 源程序 public class Leaf int i 0 Leaf increment i return this void print System
  • 除了敲代码,程序员还能有什么副业?晓健韩品批发

    积极发展副业中 做韩妆一般贸易进口 跟集装箱通关的 仓库在山东烟台莱山区 做的比较大 价格方面有优势 有需要 可以私聊哈 正品是本分 xiaojian675050734 H19950211H
  • composer 中国镜像

    我的个人博客 逐步前行STEP 使用以下代码将packagist源更换为中国镜像 composer config g repo packagist composer https packagist phpcomposer com 或者 co
  • 【满分】【华为OD机试真题2023 JAVA&JS】不爱施肥的小布

    华为OD机试真题 2023年度机试题库全覆盖 刷题指南点这里 不爱施肥的小布 知识点二分查找 时间限制 1s 空间限制 256MB 限定语言 不限 题目描述 某农场主管理了一大片果园 fields i 表示不同果林的面积 单位 m 2 现在
  • targetElement.closest

    在组件挂载时 给 document 添加点击事件监听 onMounted gt document addEventListener click closeIdentity 点击事件处理函数 用于改变 isExpanded 的值为 false
  • Lock wait timeout exceeded; try restarting transaction

    2016 08 05 09 21 52 com zhishi common controller BaseController ERROR Error updating database Cause com mysql jdbc excep
  • 教师怎样将成绩单独发送给学生和家长?

    在工作学习中 我们经常会遇到怎样将成绩单独发送给学生和家长这样的问题 因此 面对怎样将成绩单独发送给学生和家长我们应该有努力探索的精神 诚挚的心灵 是学生情感的钥匙 高尚的师德 是学生心灵的明镜 对于这个问题也是一样的 读书忌死读 死读钻牛
  • redhat7.6安装weblogic12c

    目录 一 环境准备 二 使用root创建用户和组 三 创建部署目录 四 上传安装包 五 创建 oraInst loc 文件 六 创建wls rsp 响应文件 七 进行安装 八 使用 wlst sh 离线模式创建一个域 九 启动服务 十 浏览
  • 电脑wps可以语音录入吗_怎样用word进行语音录入文字

    怎样用 word 进行语音录入文字 有时候我们不方便手敲键盘输入时 可以利用 word 的语音录入功能进行输入 以下是学习啦小编为您带来的关于用 Word 进行语音录入文字 希望对您有所帮助 用 Word 进行语音录入文字 语音输入功能添加
  • 最适合程序猿的笔记软件

    因为这几天小编要去听课 所以心血来潮找了几个适合程序猿的笔记软件 经过几天在csdn上的扒饭之后 我结合自己的某些要求 为大家整理出了这几个软件 有需要直接去后面找 一 必须支持markdown markdown的重要性不需要在这里多说了吧
  • TypeScript 联合类型(union type)

    TS是JS的超集 在JS的基础上添加了一套类型系统 这样的TS可以被静态分析带来的好处显而易见 let val string val 声明一个string类型的变量val let val string val val 1 Type numb
  • hive异常MetaException-Metastore contains multiple versions

    在执行hive运行脚本时 出现了MetaException Metastore contains multiple versions异常错误 Exception in thread main java lang RuntimeExcepti
  • Java 手动分页

    功能需求背景 今天负责短信后台定时任务时 需要定时向用户发送短信信息 但数据库记录的待发送记录数量比较大 无法一次查询出结果 需要手动分页 手动分页核心功能代码 Date now DateUtils getBeforeMouth new D
  • Arduino基本知识

    analogWrite 将一个模拟数值写进Arduino引脚 这个操作可以用来控制LED的亮度 或者控制电机的转速 Arduino每一次对引脚执行analogWrite 指令 都会给该引脚一个固定频率的PWM信号 digitalRead 读
  • 基于FPGA的图像采集之二 GEN_FRAME(成帧)模块

    距离上次的博客已经有段时间了 这写些日子一直在调SDRAM的模块以及文档的书写 SDRAM的子模块比较多 包括init 初始化模块 refresh 刷新模块 write 写模块 read 读模块 使用起来相比之前的USB控制模块 今天的GE
  • JAVA中队列,数据结构队列入队操作

    java中构造函数和构造方法的区别 Java中什么是构造函数 构造函数和普通函数的区别如下 1 写法上的不同 施工方法 Public modifier class 定义类的关键字 Test 类名 没有参数 测试 类名 接受一个参数 测试 类
  • 使用Visual Studio写一个简单的Windows窗体应用登录界面

    需要的知识 C 的基本语法 以及Visual Studio的基本操作方法 编辑软件 Sql Server 2017 Visual Studio 2017 前提 Sql Server 中有一个名为 MY LAPTOP 的服务器 一个名为 Te
  • 初学(9)——Hadoop错误:ssh: Could not resolve hostname master: Name or service not known

    进行ssh访问时出现错误 ssh Could not resolve hostname master Name or service not known 解决方法 修改hosts文件 将名称和IP建立联系 1 打开 etc目录下hosts文
  • 深入理解Go——context(2)

    文章目录 结构体 emptyCtx cancleCtx timerCtx valueCtx 结构体 emptyCtx 源码中定义了 Context 接口后 并且给出了一个实现 type emptyCtx int func emptyCtx