扫码订阅《 》或入驻星球,即可阅读文章!

GOLANG ROADMAP

阅读模式

  • 沉浸
  • 自动
  • 日常
首页
Go学习
  • Go学院

    • Go小课
    • Go小考
    • Go实战
    • 精品课
  • Go宝典

    • 在线宝典
    • B站精选
    • 推荐图书
    • 精品博文
  • Go开源

    • Go仓库
    • Go月刊
  • Go下载

    • 视频资源
    • 文档资源
Go求职
  • 求职服务

    • 内推互助
    • 求职助力
  • 求职刷题

    • 企业题库
    • 面试宝典
    • 求职面经
Go友会
  • 城市
  • 校园
推广返利 🤑
实验区
  • Go周边
消息
更多
  • 用户中心

    • 我的信息
    • 推广返利
  • 玩转星球

    • 星球介绍
    • 角色体系
    • 星主权益
  • 支持与服务

    • 联系星主
    • 成长记录
    • 常见问题
    • 吐槽专区
  • 合作交流

    • 渠道合作
    • 课程入驻
    • 友情链接
author-avatar

GOLANG ROADMAP


首页
Go学习
  • Go学院

    • Go小课
    • Go小考
    • Go实战
    • 精品课
  • Go宝典

    • 在线宝典
    • B站精选
    • 推荐图书
    • 精品博文
  • Go开源

    • Go仓库
    • Go月刊
  • Go下载

    • 视频资源
    • 文档资源
Go求职
  • 求职服务

    • 内推互助
    • 求职助力
  • 求职刷题

    • 企业题库
    • 面试宝典
    • 求职面经
Go友会
  • 城市
  • 校园
推广返利 🤑
实验区
  • Go周边
消息
更多
  • 用户中心

    • 我的信息
    • 推广返利
  • 玩转星球

    • 星球介绍
    • 角色体系
    • 星主权益
  • 支持与服务

    • 联系星主
    • 成长记录
    • 常见问题
    • 吐槽专区
  • 合作交流

    • 渠道合作
    • 课程入驻
    • 友情链接
  • Go真实面试题汇总系列

    • 《Go基础篇》
  • 宝典内容

    • 3.Go语言中是如何实现继承的?
    • 4.数组怎么转集合?
    • 13.map取一个key,然后修改这个值,原map数据的值会不会变化
    • 16. go struct 能不能比较
    • 19. 数组是如何实现用下标访问任意元素的
    • 25. 深拷贝和浅拷贝
    • 29. go 的优势
    • 30. 如何判断channel是否关闭?
    • 31. make 与 new 的区别
    • 32. Slice 与 Array, Append()
    • 36. go语言的引用类型有什么?
    • 44. 获取不到锁会一直等待吗?
    • 45. 如何实现一个 timeout 的锁?
    • 55. make可以初始化哪些结构
    • 59. 空结构体占不占内存空间? 为什么使用空结构体?
    • 61. defer 是怎么用的
    • 62. Context 包的作用
    • 65. go 语言的 panic 如何恢复
    • 66. defer 的执行顺序
    • 71. go的init函数是什么时候执行的?
    • 72. 多个init函数执行顺序能保证吗?
    • 73. gin框架的路由是怎么处理的?
    • 74. 用火焰图的优势?
    • 75. struct的传递场景
    • 77. go的profile工具
    • 78. 怎么检查go问题
    • 79. context包内部如何实现的?
    • 80. syncpool的实现原理
    • 81. go什么场景使用接口
    • 83. go怎么实现封装继承多态
    • 84. 为什么go的变量申请类型是为了什么?
    • 85. Go和JAVA垃圾回收机制有啥区别
    • 88. node.js和go是基于什么样的考虑是用这两种语言的?
    • 89. golang垃圾回收机制了解吗?
    • 100. 怎么确定走go语言技术栈的
    • 102. 介绍Gin框架
    • 108. 什么时候会触发线程切换
    • 110. defer关键字后的函数在什么时候调用 主函数return前还是return后
    • 113. golang http库设计原理,为什么不池化
    • 114. golang gc
    • 116. 关闭一个已经关闭的 Channel 会发生什么?Channel 有缓存和没缓存的区别是什么?
    • 122. 类型断言用过吗,说说实现,如何判断断言成功?
    • 123. for true {time.Sleep(1)} 有什么问题
    • 124. sleep底层实现原理
    • 126. interface 的底层实现
    • 127. STW 在 go 的哪些阶段发生?了解1.8版本的改进吗?
    • 130. go test test 和 benchmark
    • 144. for range坑输出
    • 145. go结构体和结构体指针的区别
    • 147. go如何避免panic
    • 148. 结构体创建优化
    • 152. golang interface底层实现,使用场景
    • 153. golang类型断言,怎么用
    • 154. 听说go有什么什么的缺陷,你怎么看
    • 155. 对go有哪些认识
    • 156. go和java的区别
    • 158. 对go的中间件和工作机制有了解吗?
    • 175. Go string底层实现?
    • 177. 了解HTTP协议吗?golang HTTP库实现?
    • 186. 使用range输出一个数组,需要注意的问题
    • 187. Go管理依赖go mod命令,go mod最后的版本号如果没有tag,是怎么生成的
    • 188. 进程、线程、协程的区别?
    • 195. 说一说go的defer和chan
    • 196. golang多态、父类方法重写
    • 198. 线程和协程的区别
    • 201. Golang 的结构体的组合(实现java继承的特性)
    • 202. Golang interface的设计
    • 204. context包的用途?
    • 208. Go的数据结构的零值是什么?
    • 218. 进程线程协程的区别
    • 219. go协程的好处
    • 220. byte和rune有什么区别
    • 228. 进程线程协程区别
    • 236. go中的struct 能不能比较
    • 237. go defer
    • 238. select可以用于什么
    • 242. 用go构造一个链表怎么做,想要从链表尾部插入,怎么做
    • 245. Go语言有缓冲Channel与无缓冲Channel区别
    • 253. go channel close后读的问题
    • 256. defer的执行顺序
    • 259. go 怎么实现func的自定义参数
    • 261. defer的执行顺序
    • 262. golang的调试
    • 263. defer recover panic 执行顺序
    • 267. copy是操作符还是内置函数
    • 276. Go有哪些数据结构
    • 293. defer关键字使用
    • 294. channel有缓冲、无缓冲区别
    • 295. defer和recover的配合
    • 304. 写一个东西:一个字符串json,转成一个直接可用的map,字符串可能是任何形式
    • 305. go的通信实现
    • 306. go interface的底层实现
    • 309. go func与method之前的那个receiver的作用
    • 311. 一个函数传参一个 slice,先 append 再赋值和另一个函数先赋值再append,哪个会发生变化?
    • 312. 有没有什么线程安全的办法?
    • 321. go map时间复杂度
    • 322. go 从源码到二进制代码的整个流程
    • 324. select、epoll
    • 329. make原理
    • 330. string类型转为[]byte过程发生了什么
    • 340. runtime
    • 348. go语言中结构体指针为空,赋给一个interface{}为什么interface不为空
    • 349. defer问题
    • 350. 你能介绍一下go的包管理工具吗?除了gomod还知道哪些?
    • 355. go的值传递和引用传递
    • 357. Go的闭包语法
    • 371. go的反射
    • 374. 判断下面代码的输出
    • 377. 对象是什么,面向对象有什么好处,go 中如何实现多态
    • 379. go 的执行顺序
    • 380. golang的基础问题,比如包管理,比如值传递,比如协程
    • 383. 问了golang的interface的区别,继承,gc的原理、区别,双向链表等。
    • 408. go range 的陷阱
    • 411. 考察defer和panic执行顺序的问题
    • 414. Python和Go的区别
    • 416. go的oop与传统的oop的区别
    • 417. go里面interface是什么概念
    • 418. 相比于java、c++,go的interface有什么区别吗?
    • 421. go和node的区别
    • 422. 程序计数器作用,为什么是私有的
    • 423. PHP和 Go 对比
    • 424. defer如何实现
    • 429. 讲讲go的启动过程
    • 430. Go mod主要解决了什么问题
    • 431. Go sum里面是什么内容
    • 437. Go结构体内嵌后的命名冲突
    • 440. Go 的面向对象特性
    • 442. go init 的执行顺序,注意是不按导入规则的(这里是编译时按文件名的顺序执行的)
    • 443. interface和nil 比较。
    • 454. 一个a+b程序从编译到运行都发生了什么(从预编译到print显示到屏幕上)
    • 455. Go中struct组合与Java继承的区别
    • 458. 使用过哪些 golang 的 String 类库
    • 459. golang 断言

扫码订阅《 》或入驻星球,即可阅读文章!

79. context包内部如何实现的?


企业题库解析小组

**题目序号:**935

**题目来源:**好未来

频次: 1

答案1:(趁醉独饮痛)+

context是 Go 语言在 1.7 版本中引入标准库的接口。context主要用于父子任务之间的同步取消信号,本质上是一种协程调度的方式。另外在使用context时有两点值得注意:上游任务仅仅使用context通知下游任务不再需要,但不会直接干涉和中断下游任务的执行,由下游任务自行决定后续的处理操作,也就是说context的取消操作是无侵入的;context是线程安全的,因为context本身是不可变的(immutable),因此可以放心地在多个协程中传递使用。

方法:

  • Deadline — 返回 context.Context 被取消的时间,也就是完成工作的截止日期;
  • Done — 返回一个 Channel,这个 Channel 会在当前工作完成或者上下文被取消后关闭,多次调用 Done 方法会返回同一个 Channel;
  • Err — 返回 context.Context 结束的原因,它只会在 Done 方法对应的 Channel 关闭时返回非空的值;
    • 如果 context.Context 被取消,会返回 Canceled 错误;
    • 如果 context.Context 超时,会返回 DeadlineExceeded 错误;
  • Value — 从 context.Context 中获取键对应的值,对于同一个上下文来说,多次调用 Value 并传入相同的 Key 会返回相同的结果,该方法可以用来传递请求特定的数据;

设计原理

在 Goroutine 构成的树形结构中对信号进行同步以减少计算资源的浪费是 context.Context 的最大作用。Go 服务的每一个请求都是通过单独的 Goroutine 处理的,HTTP/RPC 请求的处理器会启动新的 Goroutine 访问数据库和其他服务。

(context树结构)

图片

每一个 context.Context 都会从最顶层的 Goroutine 一层一层传递到最下层。context.Context 可以在上层 Goroutine 执行出现错误时,将信号及时同步给下层。

如果不使用 context,当最上层的 Goroutine 因为某些原因执行失败时,下层的 Goroutine 由于没有接收到这个信号所以会继续工作;但是当我们正确地使用 context.Context 时,就可以在下层及时停掉无用的工作以减少额外资源的消耗。

多个 Goroutine 同时订阅 ctx.Done() 管道中的消息,一旦接收到取消信号就立刻停止当前正在执行的工作。

默认上下文

context 包中最常用的方法还是 context.Background、context.TODO,这两个方法都会返回预先初始化好的私有变量 background 和 todo,它们会在同一个 Go 程序中被复用。

  • context.Background  是上下文的默认值,所有其他的上下文都应该从它衍生出来;
  • context.TODO  应该仅在不确定应该使用哪种上下文时使用。
func Background() Context {
	return background
}
func TODO() Context {
	return todo
} 
这两个私有变量都是通过 new(emptyCtx) 语句初始化的,它们是指向私有结构体 
context.emptyCtx 的指针,这是最简单、最常用的上下文类型。

//////////////////     emptyCtx    ///////////////////
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
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

取消信号

context.WithCancel 函数能够从 context.Context 中衍生出一个新的子上下文并返回用于取消该上下文的函数。一旦我们执行返回的取消函数,当前上下文以及它的子上下文都会被取消,所有的 Goroutine 都会同步收到这一取消信号。

Context 子树的取消

//  context.WithCancel 函数的实现
func WithCancel(parent Context) (ctx Context, cancel CancelFunc) {
	c := newCancelCtx(parent)
	propagateCancel(parent, &c)
	return &c, func() { c.cancel(true, Canceled) }
}

func propagateCancel(parent Context, child canceler) {
	done := parent.Done()
	if done == nil {
		return // 父上下文不会触发取消信号
	}
	select {
	case <-done:
		child.cancel(false, parent.Err()) // 父上下文已经被取消
		return
	default:
	}
	if p, ok := parentCancelCtx(parent); ok {
		p.mu.Lock()
		if p.err != nil {
			child.cancel(false, p.err)
		} else {
			p.children[child] = struct{}{}
		}
		p.mu.Unlock()
	} else {
		go func() {
			select {
			case <-parent.Done():
				child.cancel(false, parent.Err())
			case <-child.Done():
			}
		}()
	}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
  • context.newCancelCtx 将传入的上下文包装成私有结构体 context.cancelCtx;
  • context.propagateCancel 会构建父子上下文之间的关联,当父上下文被取消时,子上下文也会被取消:
  propagateCancel
1

propagateCancel 可能出现的情况:

  • 当 parent.Done() == nil,也就是 parent 不会触发取消事件时,当前函数会直接返回;
  • 当 child 的继承链包含可以取消的上下文时,会判断 parent 是否已经触发了取消信号;
    • 如果已经被取消,child 会立刻被取消;
    • 如果没有被取消,child 会被加入 parent 的 children 列表中,等待 parent 释放取消信号;
  • 当父上下文是开发者自定义的类型、实现了 context.Context 接口并在 Done() 方法中返回了非空的管道时;
    • 运行一个新的 Goroutine 同时监听 parent.Done() 和 child.Done() 两个 Channel;
    • 在 parent.Done() 关闭时调用 child.cancel 取消子上下文; context.propagateCancel 的作用是在 parent 和 child 之间同步取消和结束的信号,保证在 parent 被取消时,child 也会收到对应的信号,不会出现状态不一致的情况。

context.cancelCtx 实现的几个接口方法也没有太多值得分析的地方,该结构体最重要的方法是 context.cancelCtx.cancel,该方法会关闭上下文中的 Channel 并向所有的子上下文同步取消信号

// cancel 取消函数
func (c *cancelCtx) cancel(removeFromParent bool, err error) {
	c.mu.Lock()
	if c.err != nil {
		c.mu.Unlock()
		return
	}
	c.err = err
	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)
	}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

context 包中的另外两个函数 context.WithDeadline 和 context.WithTimeout 也都能创建可以被取消的计时器上下文 context.timerCtx

func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc) {
	return WithDeadline(parent, time.Now().Add(timeout))
}
func WithDeadline(parent Context, d time.Time) (Context, CancelFunc) {
	if cur, ok := parent.Deadline(); ok && cur.Before(d) {
		return WithCancel(parent)
	}
	c := &timerCtx{
		cancelCtx: newCancelCtx(parent),
		deadline:  d,
	}
	propagateCancel(parent, c)
	dur := time.Until(d)
	if dur <= 0 {
		c.cancel(true, DeadlineExceeded) // 已经过了截止日期
		return c, func() { c.cancel(false, Canceled) }
	}
	c.mu.Lock()
	defer c.mu.Unlock()
	if c.err == nil {
		c.timer = time.AfterFunc(dur, func() {
			c.cancel(true, DeadlineExceeded)
		})
	}
	return c, func() { c.cancel(true, Canceled) }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26

context.WithDeadline 在创建 context.timerCtx 的过程中判断了父上下文的截止日期与当前日期,并通过 time.AfterFunc 创建定时器,当时间超过了截止日期后会调用 context.timerCtx.cancel 同步取消信号。 context.timerCtx 内部不仅通过嵌入 context.cancelCtx 结构体继承了相关的变量和方法,还通过持有的定时器 timer 和截止时间 deadline 实现了定时取消的功能

ype 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) cancel(removeFromParent bool, err error) {
	c.cancelCtx.cancel(false, err)
	if removeFromParent {
		removeChild(c.cancelCtx.Context, c)
	}
	c.mu.Lock()
	if c.timer != nil {
		c.timer.Stop()
		c.timer = nil
	}
	c.mu.Unlock()
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

context.timerCtx.cancel 方法不仅调用了 context.cancelCtx.cancel,还会停止持有的定时器减少不必要的资源浪费。

传值方法

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

func (c *valueCtx) Value(key interface{}) interface{} {
	if c.key == key {
		return c.val
	}
	return c.Context.Value(key)
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

context.valueCtx 结构体会将除了 Value 之外的 Err、Deadline 等方法代理到父上下文中,它只会响应 context.valueCtx.Value 方法,该方法的实现也很简单 。 如果 context.valueCtx 中存储的键值对与 context.valueCtx.Value 方法中传入的参数不匹配,就会从父上下文中查找该键对应的值直到某个父上下文中返回 nil 或者查找到对应的值。