Golang 笔记(二):Context 源码剖析(1)

简介: Golang 笔记(二):Context 源码剖析(1)

Context 是 Go 中一个比较独特而常用的概念,用好了往往能事半功倍。但如果不知其然而滥用,则往往变成 "为赋新词强说愁",轻则影响代码结构,重则埋下许多bug。Context 本质上是一种在 API 间树形嵌套调用时传递信号的机制。本文将从接口、派生、源码分析、使用等几个方面来逐一解析 Context。

概述

Context 是 Go 中一个比较独特而常用的概念,用好了往往能事半功倍。但如果不知其然而滥用,则往往变成 "为赋新词强说愁",轻则影响代码结构,重则埋下许多bug。

Golang  使用树形派生的方式构造 Context,通过在不同过程 [1] 中传递 deadline 和 cancel 信号,来管理处理某个任务所涉及到的一组 goroutine 的生命周期,防止 goroutine 泄露。并且可以通过附加在 Context 上的 Value 来传递/共享一些跨越整个请求间的数据。

Context 最常用来追踪 RPC/HTTP 等耗时的、跨进程的 IO 请求的生命周期,从而让外层调用者可以主动地或者自动地取消该请求,进而告诉子过程回收用到的所有 goroutine 和相关资源。

Context 本质上是一种在 API 间树形嵌套调用时传递信号的机制。本文将从接口、派生、源码分析、使用等几个方面来逐一解析 Context。

Context 接口

Context 接口如下:

// Context 用以在多 API 间传递 deadline、cancelation 信号和请求的键值对。
// Context 中的方法能够安全的被多个 goroutine 并发调用。
 type Context interface {
       // Done 返回一个只读 channel,该 channel 在 Context 被取消或者超时时关闭
     Done() <-chan struct{}
     // Err 返回 Context 结束时的出错信息
     Err() error
    // 如果 Context 被设置了超时,Deadline 将会返回超时时限。
    Deadline() (deadline time.Time, ok bool)
    // Value 返回关联到相关 Key 上的值,或者 nil.
   Value(key interface{}) interface{}
}

上面是简略注释,接口详细信息可以访问 Context 的 godoc。

  • Done() 方法返回一个只读的 channel,当 Context 被主动取消或者超时自动取消时,该 Context 所有派生 Context 的 done channel 都被 close 。所有子过程通过该字段收到 close 信号后,应该立即中断执行、释放资源然后返回。
  • Err() 在上述 channel 被 close 前会返回 nil,在被 close 后会返回该 Context 被关闭的信息,error 类型,只有两种,被取消或者超时
var Canceled = errors.New("context canceled")
2var DeadlineExceeded error = deadlineExceededError{}
  • Deadline() 如果本 Context 被设置了时限,则该函数返回 ok=true 和对应的到期时间点。否则,返回 ok=false和 nil。
  • Value() 返回绑定在该 Context 链(我称为回溯链,下文会展开说明)上的给定的 Key 的值,如果没有,则返回 nil。注意,不要用于在函数中传参,其本意在于共享一些横跨整个 Context 生命周期范围的值。Key 可以是任何可比较的类型。为了防止 Key 冲突,最好将 Key 的类型定义为非导出类型,然后为其定义访问器。看一个通过 Context 共享用户信息的例子:
package user
 2
 3import "context"
 4
 5// User 是要存于 Context 中的 Value 类型.
 6type User struct {...}
 7
 8// key 定义为了非导出类型,以避免和其他 package 中的 key 冲突
 9type key int
10
11// userKey 是 Context 中用来关联 user.User 的 key,是非导出变量
12// 客户端需要用 user.NewContext 和 user.FromContext 构建包含
13// user 的 Context 和从 Context 中提取相应 user 
14var userKey key
15
16// NewContext 返回一个带有用户值 u 的 Context.
17func NewContext(ctx context.Context, u *User) context.Context {
18    return context.WithValue(ctx, userKey, u)
19}
20
21// FromContext 从 Context 中提取 user,如果有的话.2func FromContext(ctx context.Context) (*User, bool) {
    u, ok := ctx.Value(userKey).(*User)
    return u, ok
}

Context 派生

Context 设计之妙在于可以从已有 Context 进行树形派生,以管理一组过程的生命周期。我们上面说了单个 Context 实例是不可变的,但可以通过 context 包提供的三种方法:WithCancelWithTimeoutWithValue 来进行派生并附加一些属性(可取消、时限、键值),以构造一组树形组织的 Context。

image.png

当根 Context 结束时,所有由其派生出的 Context 也会被一并取消。也就是说,父 Context 的生命周期涵盖所有子 Context 的生命周期。

context.Background() 通常用作根节点,它不会超时,不能被取消。

// Background 返回一个空 Context。它不能被取消,没有时限,没有附加键值。Background 通常用在
// main函数、init 函数、test 入口,作为某个耗时过程的根 Context。
func Background() Context

WithCancel 和  WithTimeout  可以从父 Context 进行派生,返回受限于父 Context 生命周期的新 Context。

通过 WithCancelcontext.Background()派生出的 Context 要注意在对应过程完结后及时 cancel,否则会造成 Context 泄露。

使用 WithTimeout 可以控制某个过程的处理时限。具体过程为,到点后, Context 发送信号到 Done Channel,子过程检测到 Context Done Channel [2] 中的信号,会立即退出。

// WithCancel 返回一份父 Context 的拷贝,和一个 cancel 函数。当父 Context 被关闭或者 
// 此 cancel 函数被调用时,该 Context 的 Done Channel 会立即被关闭.
 func WithCancel(parent Context) (ctx Context, cancel CancelFunc)
// 调用 CancelFunc 取消对应 Context.
   type CancelFunc func()
 // WithTimeout 返回一份父 Context 的拷贝,和一个 cancel 函数。当父 Context 被关闭、
 // cancel 函数被调用或者设定时限到达时,该 Context 的 Done Channel 会立即关闭。在 cancel 函数
// 被调用时,如果其内部 timer 仍在运行,将会被停掉。
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc)

WithValue 可以给 Context 附加上整个处理过程中的键值。

// WithValue 返回一个父 Context 的副本,并且附加上给定的键值对.
func WithValue(parent Context, key interface{}, val interface{}) Context

Context 源码解析

Go context 使用嵌入类,以类似继承的方式组织几个 Context 类:emptyCtxvalueCtxcancelCtxtimerCtx

image.png

形象的来说,通过嵌入的方式,Go 对树形组织的 Context 体系中的每个 Context 节点都构造了一个指向父亲实例"指针"。从另一个角度来说,这是一种经典代码组织模式——组合模式,每一层只增量 or 覆盖实现自己所关注的功能,然后通过路由调用来复用已有的实现。

空实现 emptyCtx

emptyCtx 实现了一个空的 Context,所有接口函数都是空实现。

type emptyCtx int
 func (*emptyCtx) Deadline() (deadline time.Time, ok bool) {
    return
}
 func (*emptyCtx) Done() <-chan struct{} {
     return nil // 返回 nil,从语法上说是空实现,从语义上说是该 Context 永远不会被关闭。
 }
 //... 其他的省略,类似都是满足语法要求的空函数体
func (e *emptyCtx) String() string {
    switch e {3    case background:
        return "context.Background"
    case todo:
        return "context.TODO"
    }
    return "unknown empty Context"
}

context.Background()context.TODO() 返回的都是 emptyCtx 的实例。但其语义略有不同。前者做为 Context 树的根节点,后者通常在不知道用啥时用。

var (
     background = new(emptyCtx)
     todo       = new(emptyCtx)
)
 func Background() Context {
     return background
 }
func TODO() Context {
    return todo
}

附加单键值 valueCtx

valueCtx 嵌入了一个 Context 接口以进行 Context 派生,并且附加了一个 KV 对。从 context.WithValue 函数可以看出,每附加一个键值对,都得套上一层新的 valueCtx。在使用 Value(key interface) 接口访问某 Key 时,会沿着 Context 树回溯链不断向上遍历所有 Context 直到 emptyCtx

  1. 如果遇到 valueCtx 实例,则比较其 key 和给定 key 是否相等
  2. 如果遇到其他 Context 实例,就直接向上转发。但这里有个特例,为了获取给定 Context 所有祖先节点中最近的cancelCtx,go 用了一个特殊的 key:cancelCtxKey,遇到该 key 时,cancelCtx 会返回自身。这个在 cancelCtx 实现中会提到。

对于其他的接口调用(Done, Err, Deadline),会路由到嵌入的 Context 上去。

type valueCtx struct {
    Context // 嵌入,指向父 Context
    key, val interface{}
 }
 func (c *valueCtx) Value(key interface{}) interface{} {
     if c.key == key {
         return c.val
     }
    return c.Context.Value(key)
}
func WithValue(parent Context, key, val interface{}) Context {
    if key == nil {
        panic("nil key")
    }
    if !reflectlite.TypeOf(key).Comparable() {8        panic("key is not comparable")
    }
    return &valueCtx{parent, key, val} // 附加上 kv,并引用父 Context
}

可取消的 cancelCtx

context 包中核心实现在 cancelCtx 中,包括构造树形结构、进行级联取消。

type cancelCtx struct {
     Context
     mu       sync.Mutex            // 保证下面三个字段的互斥访问
     done     chan struct{}         // 惰式初始化,被第一个 cancel() 调用所关闭
     children map[canceler]struct{} // 被第一个 cancel() 调用置 nil
     err      error                 // 被第一个 cancel() 调用置非 nil
 }
func (c *cancelCtx) Value(key interface{}) interface{} {
    if key == &cancelCtxKey { 
        return c
    }
    return c.Context.Value(key)
}
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
}

Value() 函数的实现有点意思,遇到特殊 key:cancelCtxKey 时,会返回自身。这个其实是复用了 Value 函数的回溯逻辑,从而在 Context 树回溯链中遍历时,可以找到给定 Context 的第一个祖先 cancelCtx 实例。

children 保存的是子树中所有路径向下走的第一个可以 cancel 的 Context (实现了 canceler 接口,比如 cancelCtxtimerCtx 节点),可以参考后面的图来形象理解。

下面将逐一详细说明。

回溯链

回溯链是各个 context 包在实现时利用 go 语言嵌入(embedding)的特性来构造的,主要用于:

  1. Value() 函数被调用时沿着回溯链向上查找匹配的键值对。
  2. 复用 Value() 的逻辑查找最近 cancelCtx 祖先,以构造 Context 树。

valueCtxcancelCtxtimerCtx 中只有 cancelCtx直接valueCtxtimerCtx 都是通过嵌入实现,调用该方法会直接转发到 cancelCtx 或者 emptyCtx )实现了非空  Done() 方法,因此 done := parent.Done() 会返回第一个祖先 cancelCtx 中的 done channel。但如果 Context 树中有第三方实现的 Context 接口的实例时,parent.Done() 就有可能返回其他 channel。

因此,如果 p.done != done ,说明在回溯链中遇到的第一个实现非空 Done() Context 是第三方 Context ,而非 cancelCtx

// parentCancelCtx 返回 parent 的第一个祖先 cancelCtx 节点
 func parentCancelCtx(parent Context) (*cancelCtx, bool) {
   done := parent.Done() // 调用回溯链中第一个实现了 Done() 的实例(第三方Context类/cancelCtx)
     if done == closedchan || done == nil {
         return nil, false
     }
     p, ok := parent.Value(&cancelCtxKey).(*cancelCtx) // 回溯链中第一个 cancelCtx 实例
     if !ok {
         return nil, false
    }
    p.mu.Lock()
    ok = p.done == done
    p.mu.Unlock()
    if !ok { // 说明回溯链中第一个实现 Done() 的实例不是 cancelCtx 的实例
        return nil, false
    }
    return p, true
}

树构建

Context 树的构建是在调用 context.WithCancel() 调用时通过 propagateCancel 进行的。

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

Context 树,本质上可以细化为 canceler*cancelCtx*timerCtx)树,因为在级联取消时只需找到子树中所有的 canceler ,因此在实现时只需在树中保存所有 canceler 的关系即可(跳过 valueCtx),简单且高效。


相关文章
|
4天前
|
存储 SQL 安全
Golang底层原理剖析之上下文Context
Golang底层原理剖析之上下文Context
67 0
|
4天前
|
Linux Go
浅谈Golang上下文Context
浅谈Golang上下文Context
27 0
|
4天前
|
SQL 前端开发 Go
编程笔记 GOLANG基础 001 为什么要学习Go语言
编程笔记 GOLANG基础 001 为什么要学习Go语言
|
4天前
|
数据管理 Go 开发者
Golang深入浅出之-Go语言上下文(context)包:处理取消与超时
【4月更文挑战第25天】Go语言中的`context`包在并发、网络请求和长任务中至关重要,提供取消、截止时间和元数据管理。本文探讨`context`基础,如`Background()`、`TODO()`、`WithCancel()`、`WithDeadline()`和`WithTimeout()`。常见问题包括不当传递、过度使用`Background()`和`TODO()`以及忽略错误处理。通过取消和超时示例,强调正确传递上下文、处理取消错误和设置超时以提高应用健壮性和响应性。正确使用`context`是构建稳定高效Go应用的关键。
22 1
|
4天前
|
Go 开发者
Golang深入浅出之-Go语言上下文(context)包:处理取消与超时
【4月更文挑战第23天】Go语言的`context`包提供`Context`接口用于处理任务取消、超时和截止日期。通过传递`Context`对象,开发者能轻松实现复杂控制流。本文解析`context`包特性,讨论常见问题和解决方案,并给出代码示例。关键点包括:1) 确保将`Context`传递给所有相关任务;2) 根据需求选择适当的`Context`创建函数;3) 定期检查`Done()`通道以响应取消请求。正确使用`context`包能提升Go程序的控制流管理效率。
21 1
|
7月前
|
监控 安全 Go
Golang 语言中 Context 的使用方式
Golang 语言中 Context 的使用方式
24 0
|
4天前
|
存储 IDE 编译器
编程笔记 GOLANG基础 005 第一个程序:hello world 使用vscode
编程笔记 GOLANG基础 005 第一个程序:hello world 使用vscode
|
4天前
|
Go 开发工具
编程笔记 GOLANG基础 004 GOLANG常用命令及VSCODE快捷键
编程笔记 GOLANG基础 004 GOLANG常用命令及VSCODE快捷键
|
4天前
|
Go 开发工具 git
编程笔记 GOLANG基础 003 Go语言开发环境搭建
编程笔记 GOLANG基础 003 Go语言开发环境搭建
|
4天前
|
存储 Java Go
编程笔记 GOLANG基础 002 Go语言简介
编程笔记 GOLANG基础 002 Go语言简介