本篇文章是基于小徐先生的文章的修改和个人注解,要查看原文可以点击上述的链接查看
目前我这篇文章的go语言版本是1.24.1
context上下文
context被当作第一个参数(官方建议),并且不断的传递下去,基本上一个项目代码到处都是context,但是你们真的知道他有什么作用吗?
接下来就来看看这个golang世界中的典型工具吧
func main() {
ctx,cancel := context.WithTimeout(context.Background(),10 * time.Second)
defer cancel()
go Monitor(ctx)
time.Sleep(20 * time.Second)
}
func Monitor(ctx context.Context) {
for {
fmt.Print("monitor")
}
}
但是他到底时如何处理并发控制和实现呢?
接下来就来深入看看他的原理和使用
一.context包介绍
context
可以用来在goroutine
之间传递上下文信息,相同的context
可以传递给运行在不同goroutine
中的函数,上下文对于多个goroutine
同时使用是安全的,context
包定义了上下文类型,可以使用background
、TODO
创建一个上下文,在函数调用链之间传播context
,也可以使用WithDeadline
、WithTimeout
、WithCancel
或 WithValue
创建的修改副本替换它,听起来有点绕,其实总结起就是一句话:context
的作用就是在不同的goroutine
之间同步请求特定的数据、取消信号以及处理请求的截止日期。
目前我们常用的一些库都是支持context
的,例如gin
、database/sql
等库都是支持context
的,这样更方便我们做并发控制了,只要在服务器入口创建一个context
上下文,不断透传下去即可。
二.context的使用
2.1 context.Context
他是核心数据结构,看一下它的样子吧:

type Context interface {
Deadline() (deadline time.Time, ok bool)
Done() <-chan struct{}
Err() error
Value(key any) any
}
Context 为 interface,定义了四个核心 api:
• Deadline:返回 context 的过期时间
• Done:返回 context 中的 channel
• Err:返回错误
• Value:返回 context 中的对应 key 的值
2.2 标准error
var Canceled = errors.New("context canceled")
var DeadlineExceeded error = deadlineExceededError{}
type deadlineExceededError struct{}
func (deadlineExceededError) Error() string { return "context deadline exceeded" }
func (deadlineExceededError) Timeout() bool { return true }
func (deadlineExceededError) Temporary() bool { return true }
• Canceled:context 被 cancel 时会报此错误
• DeadlineExceeded:context 超时时会报此错误
三.类的实现
3.1 emptyCtx
通过看它的源码我们会发现,这个空的上下文其实就是实现了这个context这个接口,对于实现的4个方法都是返回的nil,这就是为什么说他是empty
在之前的版本中他可能是int类型,后来已经被修改了
type emptyCtx struct{}
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 any) any {
return nil
}
• emptyCtx 是一个空的 context
• Deadline 方法会返回一个公元元年时间以及 false 的 flag,标识当前 context 不存在过期时间;
• Done 方法返回一个 nil 值,用户无论往 nil 中写入或者读取数据,均会陷入阻塞;
• Err 方法返回的错误永远为 nil;
• Value 方法返回的 value 同样永远为 nil.
3.1.1 context.Background() & context.TODO()
context包主要提供了两种方式创建context:
context.Backgroud()
context.TODO()
我们在看代码的时候,经常可以看到不是Background就是todo作为上下文的起始,那他们有什么区别呢?
两者的区别
这两个函数其实只是互为别名,没有差别,官方给的定义是:
context.Background
是上下文的默认值,所有其他的上下文都应该从它衍生(Derived)出来。context.TODO
应该只在不确定应该使用哪种上下文时使用;
所以在大多数情况下,我们都使用context.Background
作为起始的上下文向下传递。
看一下两者的底层是什么?
两者其实都是一个对context的一个继承
type backgroundCtx struct{ emptyCtx }
type todoCtx struct{ emptyCtx }
func Background() Context {
return backgroundCtx{}
}
func TODO() Context {
return todoCtx{}
}
看到这里,你会发现上面的两种方式是创建根context
,不具备任何功能,具体实践其实还是要依靠context
包提供的With
系列函数来进行派生:
func WithCancel(parent Context) (ctx Context, cancel CancelFunc)
func WithDeadline(parent Context, deadline time.Time) (Context, CancelFunc)
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc)
func WithValue(parent Context, key, val interface{}) Context
这四个函数都要基于父Context
衍生,通过这些函数,就创建了一颗Context树,树的每个节点都可以有任意多个子节点,节点层级可以有任意多个,画个图表示一下:

基于一个父Context
可以随意衍生,其实这就是一个Context
树,树的每个节点都可以有任意多个子节点,节点层级可以有任意多个,每个子节点都依赖于其父节点,例如上图,我们可以基于Context.Background
衍生出四个子context
:ctx1.0-cancel
、ctx2.0-deadline
、ctx3.0-timeout
、ctx4.0-withvalue
,这四个子context
还可以作为父context
继续向下衍生,即使其中ctx1.0-cancel
节点取消了,也不影响其他三个父节点分支。
创建context
方法和context
的衍生方法就这些,关于这些with函数,会在后续的使用中看到
3.1.2 With类函数
WithCancel(parent Context)
用途:创建一个新的上下文和取消函数。当调用取消函数时,所有派生自这个上下文的操作将被通知取消。
应用场景:当一个长时间运行的操作需要能够被取消时。例如,用户在网页中点击"取消"按钮时,相关的数据库或 HTTP 请求应立即停止。
WithDeadline(parent Context, d time.Time)
用途 :创建一个新的上下文,该上下文在指定的时间点自动取消。
应用场景:在请求处理时设置最大执行时间。例如,调用外部 API 时,如果响应时间超过预期,将自动取消请求,以避免无效的等待。
WithTimeout(parent Context, timeout time.Duration)
用途 :创建一个新的上下文,它会在指定的持续时间内自动取消。
应用场景:适用于设置操作的超时时间,确保系统不会在某个操作上无休止地等待。常用于网络请求或长时间运行的任务。
WithValue(parent Context, key, val interface{})
用途:创建一个新的上下文,并将键值对存储在该上下文中。
应用场景:在处理请求时,将特定的数据(如用户身份信息、RequestID)在处理链中传递,而不需要在每个函数参数中显式传递。
3.2 cancelCtx
接下来看一下第二个实现的类吧,看名字就能看出来他是一个带有取消功能的上下文。

type cancelCtx struct {
Context
mu sync.Mutex
done atomic.Value
children map[canceler]struct{} // 这里是一个set{}
err error
cause error
}
type canceler interface {
cancel(removeFromParent bool, err, cause error)
Done() <-chan struct{}
}
// 这里体现了goland的一个编程哲学
// 作为一个父context,它只需要关注子类的这两个方法即可,
// 它的子类可能更有能力,但是与父亲无关,只需要知道他是否还存在即可
// 会通过就近生成interface的方式,把无关的信息都屏蔽掉。
// 也就是谁使用谁声明谁管理
• 继承了一个 context 作为其父 context. 可见,cancelCtx 必然为某个 context 的子 context;
• 内置了一把锁,用以协调并发场景下的资源获取;
• done:实际类型为 chan struct{},即用以反映 cancelCtx 生命周期的通道;
• children:一个 set,指向 cancelCtx 的所有子 context;
• err:记录了当前 cancelCtx 的错误. 必然为某个 context 的子 context;
• cause:是在go1.20之后加入的字段,主要作用适用于记录导致context被取消的具体原因
在这里,要加入一些其他的内容----同步调用和异步调用
同步调用,其实就是一种串行的方式,也就是我们平时写的程序,他是一步一步进行下去,类似一条链的形式。
而异步调用则是开辟协程,并且不会阻塞主线程,并且主线程对子协程的感知能力很弱,开辟了多个子协程,就会形成类似树的形式

就会导致,主线程对子协程的管理能力下降,从而致使协程无法回收,最后导致协程泄露的一个问题。
在创建协程方面,我们要知道一点,如果不知道你创建的协程什么时候结束,你就不应该去创建,不应该滥用并发。
如何解决这个协程的控制呢?
那么今天的主角就是cancelCtx了
先说cancelCtx继承了Context,对其方法进行了一个重写,但是并没有对Deadline方法重写,而是直接继承的父类的。Deadline不进行重写是因为他没有过期取消的能力。
func (c *cancelCtx) Value(key any) any {
// 这里为什么要加入这个判断,在后续会介绍,主要就是用于判断
// 其自身是否是一个cancelCtx类型,这个cancelCtxKey是一个定值
if key == &cancelCtxKey {
return c
}
return value(c.Context, key)
}
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{})
c.done.Store(d)
}
return d.(chan struct{})
}
func (c *cancelCtx) Err() error {
c.mu.Lock()
err := c.err
c.mu.Unlock()
return err
}
3.2.1 WithCancel取消控制
既然想实现这种父子联动的行为,就轮到了With下的一个函数WithCancel函数,通过这个函数从而得到一个cancelCtx对象,从而实现一个对子协程的一个控制
func main() {
ctx,cancel := context.WithCancel(context.Background())
// 以context.Background()为父,创建得到一个子context和cancel
go Speak(ctx)
time.Sleep(10*time.Second)
cancel()
time.Sleep(1*time.Second)
}
func Speak(ctx context.Context) {
for range time.Tick(time.Second){
select {
case <- ctx.Done():
fmt.Println("我要闭嘴了")
return
default:
fmt.Println("balabalabalabala")
}
}
}
来对这个例子做出一个解释:
select就相当于是一个多路复用,进行一个监听的操作,通过这个WithCancel获取上下文和取消函数
当调用这个cancel函数的时候,就会直接通过这个ctx.Done发送这个取消机制,从而实现一个控制的效果
这里的操作也就是我们常说的超时控制了,当然cancel并没有涉及到超时,他是通过调用cancel()才可以实现一个关闭。
这个结构体就是cancel取消函数的结构体,他返回的是一个函数类型,所以调用的时候需要加上()
来看下这个函数的具体流程这里先提前告知一点,如果停止一个cancelCtx,则这个它下面所有的子上下文都将被杀死,具体的操作看propagateCancel函数
type CancelFunc func()
func WithCancel(parent Context) (ctx Context, cancel CancelFunc) {
c := withCancel(parent)
//Canceled是一个error,自定义的错误信息
return c, func() { c.cancel(true, Canceled, nil) }
}
func withCancel(parent Context) *cancelCtx {
if parent == nil {
panic("cannot create context from nil parent")
}
c := &cancelCtx{}
c.propagateCancel(parent, c)
return c
}
func (c *cancelCtx) propagateCancel(parent Context, child canceler) {
c.Context = parent
// 这一步就是说父亲是emptyCtx,他就没有必要取消
// 所以没有必要大费周折的取消它,直接就返回就行
done := parent.Done()
if done == nil {
return // parent is never canceled
}
// 如果父亲被取消了,那儿子也应该直接取消,记录一下取消的错误和原因
select {
case <-done:
// parent is already canceled
child.cancel(false, parent.Err(), Cause(parent))
return
default:
}
// 如果我的父也是cancelCtx,则我需要将孩子加入map里面
if p, ok := parentCancelCtx(parent); ok {
// parent is a *cancelCtx, or derives from one.
p.mu.Lock()
if p.err != nil {
// parent has already been canceled
child.cancel(false, p.err, p.cause)
} else {
if p.children == nil {
p.children = make(map[canceler]struct{})
}
p.children[child] = struct{}{}
}
p.mu.Unlock()
return
}
//检查父Context是否支持AfterFunc,也就是一个回调机制
//
if a, ok := parent.(afterFuncer); ok {
// parent implements an AfterFunc method.
c.mu.Lock()
stop := a.AfterFunc(func() {
child.cancel(false, parent.Err(), Cause(parent))
})
c.Context = stopCtx{
Context: parent,
stop: stop,
}
c.mu.Unlock()
return
}
// 开启一个守护协程,时刻监听,第一个判断父亲是不是被终止了
// 如果父亲被终止了,就应该给孩子也砍一刀,让他们也都终止
// 如果孩子被终止了,那就被终止了,什么也不需要处理,传播具有单向性
goroutines.Add(1)
go func() {
select {
case <-parent.Done():
child.cancel(false, parent.Err(), Cause(parent))
case <-child.Done():
}
}()
}
parentCancelCtx用于判断是不是cancelCtx类型
func (c *cancelCtx) Value(key any) any {
// 这里为什么要加入这个判断,在后续会介绍,主要就是用于判断
// 其自身是否是一个cancelCtx类型,这个cancelCtxKey是一个定值
if key == &cancelCtxKey {
return c
}
return value(c.Context, key)
}
func parentCancelCtx(parent Context) (*cancelCtx, bool) {
done := parent.Done()
if done == closedchan || done == nil {
return nil, false
}
p, ok := parent.Value(&cancelCtxKey).(*cancelCtx)
if !ok {
return nil, false
}
pdone, _ := p.done.Load().(chan struct{})
if pdone != done {
return nil, false
}
return p, true
}
再看一下返回的闭包函数吧
cancelCtx.cancel 方法有三个入参,第一个 removeFromParent 是一个 bool 值,表示当前 context 是否需要从父 context 的 children set 中删除;第二个 err 则是 cancel 后需要展示的错误,第三个则表示导致错误原因。
func (c *cancelCtx) cancel(removeFromParent bool, err, cause error) {
if err == nil {
panic("context: internal error: missing cancel error")
}
if cause == nil {
cause = err
}
c.mu.Lock()
if c.err != nil {
c.mu.Unlock()
return // already canceled
}
c.err = err
c.cause = cause
d, _ := c.done.Load().(chan struct{})
if d == nil {
// closedchan 这里是一个全局chan
// 关闭,从而取消监听,Store就是为了确保原子性和可见性
c.done.Store(closedchan)
} else {
close(d)
}
for child := range c.children {
// NOTE: acquiring the child's lock while holding parent's lock.
child.cancel(false, err, cause)
}
c.children = nil
c.mu.Unlock()
if removeFromParent {
removeChild(c.Context, c)
}
}
// removeChild removes a context from its parent.
func removeChild(parent Context, child canceler) {
if s, ok := parent.(stopCtx); ok {
s.stop()
return
}
p, ok := parentCancelCtx(parent)
if !ok {
return
}
p.mu.Lock()
if p.children != nil {
delete(p.children, child)
}
p.mu.Unlock()
}
3.3 timerCtx
接下来看第三个实现的类

type timerCtx struct {
cancelCtx
timer *time.Timer // Under cancelCtx.mu.
deadline time.Time
}
timerCtx 在 cancelCtx 基础上又做了一层封装,除了继承 cancelCtx 的能力之外,新增了一个 time.Timer 用于定时终止 context;另外新增了一个 deadline 字段用于字段 timerCtx 的过期时间.
这样就有了实现时停的操作,它对Dealine进行了一个重写,其他都是继承的cancelCtx的
Deadline返回的是 deadline time.Time
3.3.1WithTimeout和WithDeadline
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) {
return WithDeadlineCause(parent, d, nil)
}
这两个函数他们的参数就可以看出区别,context.WithTimeout是指经过的时间段,而WithDeadline则是指时间点。
这里属于是超时取消。
3.4 valueCtx

type valueCtx struct {
Context
key, val any
}

说一下这个valueCtx吧,在不同位置设置的value其实在查找方面是有问题的,比如你在最下面那一层去存放,也就是B下面的子节点存放value,只有A和B才可以访问这个value,C和D是无法访问到的。

3.4.1WithValue
func WithValue(parent Context, key, val any) Context {
if parent == nil {
panic("cannot create context from nil parent")
}
if key == nil {
panic("nil key")
}
if !reflectlite.TypeOf(key).Comparable() {
panic("key is not comparable")
}
return &valueCtx{parent, key, val}
}
通过这个函数来设置value值。