Go言语Context包源码学习
0前语
context包作为运用go进行server端开发的重要东西,其源码只要791行,不包括注释的话估计在500行左右,十分值得咱们去深化探讨学习,所以在本篇笔记中咱们一起来调查源码的完结,知其然更要知其所以然。(当时运用go版别为1.22.2)
1中心数据结构
全体的接口完结和结构体embed图
1.1Context接口
context接口界说了四个办法:
- Deadline办法回来context是否为timerctx,以及它的完毕时刻
- Done办法回来该ctx的done channel
- Err办法回来该ctx被撤销的原因
- Value办法回来key对应的value
2emptyCtx
先来调查源码
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接口中的一切办法,关于每个办法回来的都是空值,它没有值、不能被撤销以及没有截止时刻,它只作为一个空context的载体,相当于一切ctx的先人。
怎么创立一个context?
context包供给了Background()办法和TODO()办法,都用于创立一个空的context。
func Background() Context {
return backgroundCtx{}
}
func TODO() Context {
return todoCtx{}
}
type backgroundCtx struct{ emptyCtx }
func (backgroundCtx) String() string {
return "context.Background"
}
type todoCtx struct{ emptyCtx }
func (todoCtx) String() string {
return "context.TODO"
}
它们回来的都为空的context,尽管办法名不同,可是效果是相同的,那么什么时分运用background,什么时分运用TODO呢,这是官方给出的注释
“TODO 会回来一个非空的、为空的 [Context]。 代码应该在不清楚应该运用哪个 [Context] 或许 [Context] 没有可用(由于周围的函数没有被扩展以承受 [Context] 参数)时运用 context.TODO。”
“background回来一个非空的、空的上下文目标。它不会被撤销,没有值,也没有截止时刻。它一般被主函数、初始化和测验运用,作为进入恳求的尖端上下文。”
3cancelctx
先来调查cancelctx结构体的完结
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
cause error // set to non-nil by the first cancel call
}
-
cancelCtx 内嵌了 Context 作为其父 context,依据go言语的特性,cancelCtx结构体就隐式完结了context接口中的一切办法,能够被当作一个接口来被调用,可是其办法还需求被详细的完结赋值才干进行调用。而且能够得知,cancelCtx的父类必定也是一个Context。
-
mu是cancelCtx的内置锁,用来和谐并发场景下的资源获取
-
done的实践类型为chan struct{},经过atomic包来完结并发安全,能够用于反应该ctx的生命周期状况,done是懒汉式创立的,只要榜首次调用Done()办法时才会被创立,在下文的Done办法中会说到
-
children用于相关和子ctx的联络,当撤销该ctx时,能够连续告诉子ctx进行封闭,及时开释资源。
-
err用于回来ctx封闭的原因,调用的是context包界说的内置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 }
-
cause回来的是该ctx失效更底层的原因,例如导致DeadlineExceeded err的详细原因是“database connection timeout”
// Example use: // // ctx, cancel := context.WithCancelCause(parent) // cancel(myError) // ctx.Err() // returns context.Canceled // context.Cause(ctx) // returns myError
3.1Done办法的完结
func (c *cancelCtx) Done() <-chan struct{} {
d := c.done.Load() //获取cancelCtx的done通道
if d != nil { //假若现已创立过了done通道,则直接回来
return d.(chan struct{})
}
c.mu.Lock() //上锁
defer c.mu.Unlock()
d = c.done.Load() //Double check是否创立done通道,由于在上锁前,或许其他goroutine调用了该ctx的Done办法。
if d == nil { //假设仍然未创立
d = make(chan struct{}) //创立该done channel
c.done.Store(d) //存储
}
return d.(chan struct{})
}
经过代码能够看见,ctx的done只要当被调用过Done办法时才会被创立,那么为什么这姿态规划呢?很简单想到首要意图便是为了节省了不必要的资源糟蹋,进步功率,在许多状况下创立context并不需求监听done通道,只要在需求时才被创立,契合go言语的规划理念,只要需求的时分才引进。
3.2value办法的完结
func (c *cancelCtx) Value(key any) any {
if key == &cancelCtxKey {
return c
}
return value(c.Context, key)
}
若参数key与cancelCtxKey相符,则回来当时ctx自身
不然,就向父层 层层寻觅
源码体现的十分不流畅,为了详细知道这个Value办法是做什么的,咱们先来看关于cancelCtxKey的界说
// &cancelCtxKey is the key that a cancelCtx returns itself for.
var cancelCtxKey int
cancelCtxKey是一个私有的、仅有的标识符,它用于回来cancelCtx它自身。
关于该cancelCtxKey详细运用场景,下面还会讲到
3.3创立cancelCtx的WithCancel
//WithCancel回来一个带有新的Done通道的父context的副本。只要当父ctx被封闭,或许回来的cancel办法被调用时,该ctx的Done通道才会被封闭。
func WithCancel(parent Context) (ctx Context, cancel CancelFunc) {
c := withCancel(parent)
return c, func() { c.cancel(true, Canceled, nil) }
}
该办法回来了一个cancelCtx,以及封闭它的cancel办法。
在1.20版别中,新增了一个WithCancelCause办法,该办法回来了一个cancelctx和它的CancelCauseFunc,咱们也来看一下
func WithCancelCause(parent Context) (ctx Context, cancel CancelCauseFunc) {
c := withCancel(parent)
return c, func(cause error) { c.cancel(true, Canceled, cause) }
}
在代码方面根本和withCancel办法共同,可是回来的CancelCauseFunc能够用于给用户自界说ctx被撤销的原因,例如
ctx, cancel := context.WithCancelCause(parent)
cancel(myError)
ctx.Err() // returns context.Canceled
context.Cause(ctx) // returns myError
接着来看withCancel
func withCancel(parent Context) *cancelCtx {
if parent == nil { //若父ctx是nil,那么不能创立
panic("cannot create context from nil parent")
}
c := &cancelCtx{}
c.propagateCancel(parent, c) //传递cancel给新创立的ctx
return c
}
首要来看propagateCancel做了什么:
//该办法首要用于树立父context和子context的联络,假设父context也是一个cancelCtx,它需求确保父context被撤销时,子context也能跟着被撤销。
func (c *cancelCtx) propagateCancel(parent Context, child canceler) {
c.Context = parent //将父context内嵌入子context中
done := parent.Done() //获取父ctx的Done通道
if done == nil { //假设通道不存在,那么阐明父context不是cancelCtx,不需求为它们两个之间树立联络,由于父context永久不会封闭。
return // parent is never canceled
}
select {
case <-done: //非阻塞地获取父done通道的状况,假设done通道以及被closed,那么这儿会承受到一个零值,假设没有被closed,会履行default后边的句子。
//承受到零值,阐明done通道以及被封闭了,父context现已被撤销,此刻应该当即调用子context的cancel办法,撤销子context。
child.cancel(false, parent.Err(), Cause(parent))
return
default:
}
//不然,履行以下代码
//此处判别父context是否是一个context包完结的cancelCtx,怎么了解会在下文叙述该办法时持续阐明
if p, ok := parentCancelCtx(parent); ok {
// 假设父context是一个cancelCtx,或许是从某个cancelCtx衍生出来的context
p.mu.Lock() //加锁
if p.err != nil {
// 假设存在err,阐明现已被撤销,此刻也应该调用child的cancel办法撤销子ctx
child.cancel(false, p.err, p.cause)
} else {
if p.children == nil { //假设这是p的榜首个子cancelCtx,需求初始化map的内存
p.children = make(map[canceler]struct{})
}
p.children[child] = struct{}{}//绑定子ctx到父ctx的map中,用于当父ctx撤销时,能告诉一切的child跟着撤销。
}
p.mu.Unlock()
return
}
//假设父ctx不是cancelctx而且完结了AfterFuncer接口,即完结了AfterFunc办法(该办法会在ctx被撤销后仅有一次调用),那么就需求为父ctx再设置一个afterFunc办法,用于撤销child而且传递err和cause
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, //这儿的stop办法能够用于当父ctx调用afterFunc的时分,撤销父ctx对cancel函数的调用(看需求)
}
c.mu.Unlock()
return
}
//下面的状况为添加一个goroutine,监听父ctx自己完结的Done channel(用户自界说的)
goroutines.Add(1)
go func() {
select {
case <-parent.Done()://假设监听到父ctx自界说完结的Done channel封闭时,就封闭child
child.cancel(false, parent.Err(), Cause(parent))
case <-child.Done(): //假设child先封闭,那么就当即开释协程,防止协程走漏
}
}()
}
propagateCancel办法首要的效果是,确保了关于父ctx被撤销时,为了能及时撤销子ctx,防止不必要的资源糟蹋,树立父ctx和子ctx之间的联络。运用流程图来表明该办法如下(省掉了检查afterFunc)
在该办法中,比较难以了解的当地是第二个if,"if p, ok := parentCancelCtx(parent); ok"终究做了什么,为此咱们跟进源码检查:
// parentCancelCtx 回来父级目标的底层 cancelCtx。
// 它经过查找 parent.Value(&cancelCtxKey) 来找到最内层的 enclosing cancelCtx,然后检查 parent.Done() 是否与该 cancelCtx 匹配。(假设不匹配,则该 cancelCtx 或许已被封装在供给了不同 done 通道的自界说完结中,在这种状况下,咱们不应该绕过它。)
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
}
三个if别离做了什么:
-
榜首个if:获取parentCtx的done channel,而且检查状况,若现已封闭或许父Ctx不行撤销,此刻回来false,回到propagate办法中终究会cancel掉child
-
第二个if:能够了解为经过Value办法找到离parentCtx“最近的”cancelCtx p,一般状况下,假设parentCtx便是一个CancelCtx,这时分便是parentCtx它自身。假设p不存在,也便是没有能够撤销的ctx,此刻也会回来false。
-
第三个if:找到了p后,还读取了p的done channel,这时分一般状况,pdone 当然会 == done,因而终究会回来p和true,那么什么时分会不相等呢?为什么会不相等呢?为此,看下方的层次图来了解这个if
在这个状况假设下,ParentCtx2是从ParentCtx1衍生出来的,ParentCtx1是一个规范的CancelCtx,而ParentCtx2是一个用户自界说了ctx,它内层承继了ParentCtx1而且自己完结了Done()办法,这时分代码中的“p”找到的便是ParentCtx1,而parent是ParentCtx2,此刻的done和pdone便是两个不同的channel了,这时分cihldctx应该监听哪一个done channel呢?答案是监听用户自界说完结的Ctx的chennel,由于咱们不应该绕过用户完结的Done channel,这愈加契合ctx到层次逻辑。假设这时分不去判别pdone == done,直接回来的指针便是ParentCtx1的指针了。
3.4Cancel办法完结
接下来咱们来看cancel办法是怎么完结的。
//cancel 封闭 c.done,cancel 每一个c的children。假设removeFromParent为true,将会把c从parentCtx的child中移除。
func (c *cancelCtx) cancel(removeFromParent bool, err, cause error) {
if err == nil { //有必要存在err
panic("context: internal error: missing cancel error")
}
if cause == nil { //没有自界说设置cause,默以为err
cause = err
}
c.mu.Lock() //上锁
if c.err != nil { //double check
c.mu.Unlock()
return // already canceled
}
c.err = err
c.cause = cause
d, _ := c.done.Load().(chan struct{})
if d == nil { //done没有被创立,直接存储context包内以及创立了的封闭的channel,不需求再次创立
c.done.Store(closedchan)
} else {
close(d) //封闭done
}
for child := range c.children { //封闭每一个子context
// 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) //从父child中移除自己
}
}
代码比较简单,问题在于什么时分removFromParent为true呢?为什么为true?
回到withCancel办法中,咱们能够看到回来的cancel办法中,此刻removeFromParent就为true
return c, func() { c.cancel(true, Canceled, nil) }
当用户主动调用cancel()时,就会将子ctx从父ctx中的child删去。由于此刻没有必要再在父ctx中承受父或先人的cancel告诉。而当调用cancel函数内部,对child履行的cancel就为false,这是由于后边设置了c.children = nil,这时分是从父ctx的方向封闭了子ctx对其的链接。
4afterFuncCtx
afterFuncCtx是1.20版别后引进的新ctx,它的效果是当ctx被撤销后,能履行一次自界说的F函数,一般用于收回资源等。
type afterFuncCtx struct {
cancelCtx
once sync.Once // either starts running f or stops f from running
f func()
}
能够看到afterFuncCtx embed了cancelCtx,在此基础上添加了once和f。once确保了f只会被履行一次。接着咱们来看怎么完结一个afterFuncCtx
4.1AfterFunc
func AfterFunc(ctx Context, f func()) (stop func() bool) {
a := &afterFuncCtx{ //创立局部变量afterFuncCtx,记载cancel时需求被履行的func
f: f,
}
a.cancelCtx.propagateCancel(ctx, a)
return func() bool { //回来stop办法,假设需求不履行func,则调用stop
stopped := false
a.once.Do(func() { //测验stop func函数,假设成功则stop会被设置为true
stopped = true
})
if stopped { //stop后,a没有存在的含义,进行撤销。
a.cancel(true, Canceled, nil)
}
return stopped //true为成功撤销,false表明f现已被履行或许正在被履行。
}
}
这是一个闭包完结,闭包是指在函数内部界说的函数(如这儿回来的 stop
函数),它会“捕获”并保存界说时可拜访的一切外部变量。在 AfterFunc
办法中,尽管 a
是局部变量,但回来的 stop
办法引用了 a
,形成了一个闭包,闭包会将 a
的内存保留在堆上,即便 AfterFunc
办法回来后,a
仍然存在。所以当尽管没有回来a,可是回来的stop办法任然能调用a,a的生命周期超出了afterFunc办法。
以下是一个示例
package main
import (
"context"
"fmt"
"time"
)
func main() {
// 创立一个 2 秒后超时的 context
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
// 注册 AfterFunc,context 完结时将调用整理操作
stop := context.AfterFunc(ctx, func() {
fmt.Println("整理操作正在履行...")
})
// 等候 3 秒
time.Sleep(3 * time.Second)
// 测验中止整理操作
stopped := stop()
if stopped {
fmt.Println("成功中止整理操作")
} else {
fmt.Println("整理操作现已开端或已中止")
}
// 等候 context 超时
<-ctx.Done()
fmt.Println("程序完毕")
}
4timerCtx
接下来来看timerCtx
type timerCtx struct {
cancelCtx
timer *time.Timer // Under cancelCtx.mu.
deadline time.Time
}
timer直接内嵌了cancelCtx以完结Done和Err,而且新添了timer和deadline字段,deadline用于检查ctx的截止时刻,timer用于完结过期撤销ctx。
4.1WithDeadline
先来看WithTimeout办法
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc) {
return WithDeadline(parent, time.Now().Add(timeout))
}
WithTimeout承受一个存活时刻来创立一个timerCtx,能够看到终究都是调用了WithDeadline
func WithDeadline(parent Context, d time.Time) (Context, CancelFunc) {
return WithDeadlineCause(parent, d, nil)
}
WithDeadline又调用了WithDeadlineCause,回来了timerCtx和一个CancelFunc。
4.2WithDeadlineCause
func WithDeadlineCause(parent Context, d time.Time, cause error) (Context, CancelFunc) {
if parent == nil {
panic("cannot create context from nil parent")
}
if cur, ok := parent.Deadline(); ok && cur.Before(d) { //假设parent的deadline更早,则直接回来parent的副本,不需求再创立timer
// The current deadline is already sooner than the new one.
return WithCancel(parent)
}
c := &timerCtx{
deadline: d,
}
c.cancelCtx.propagateCancel(parent, c)
dur := time.Until(d)
if dur <= 0 {//检测是否在创立进程中现已过了ddl
c.cancel(true, DeadlineExceeded, cause) // deadline has already passed
return c, func() { c.cancel(false, Canceled, nil) }
}
c.mu.Lock()
defer c.mu.Unlock()
if c.err == nil {
c.timer = time.AfterFunc(dur, func() { //设置超时主动cancel
c.cancel(true, DeadlineExceeded, cause)
})
}
return c, func() { c.cancel(true, Canceled, nil) }//回来timerCtx和cancelfunc
}
比较好了解,所以接着往下看cancel办法
4.3.cancel
func (c *timerCtx) cancel(removeFromParent bool, err, cause error) {
c.cancelCtx.cancel(false, err, cause)
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()
}
这儿值得注意的当地只要第二行,它调用的是c.cancelCtx.cancel,撤销的并非是父ctx,而是它自身,这儿的效果是为了区别cancel办法的完结,c.cancelCtx是父ctx的一个副本,并不是父ctx,所以真实的parent是c.cancelCtx.Context。
5valueCtx
type valueCtx struct {
Context
key, val any
}
valueCtx内嵌了Context接口所以具有该接口的一切办法,以及添加了k-v pair。
5.1WithValue
// 供给的键有必要可比较,而且不应是字符串或任何其他内置类型,以防止在运用上下文时与其他包发生冲突。运用WithValue的用户应为其键界说自己的类型。为了防止在将值赋给接口{}时分配内存,上下文键一般具有详细的类型struct{}。或许,应将导出的上下文键变量的静态类型设置为指针或接口。
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() { //有必要确保key是可比较的
panic("key is not comparable")
}
return &valueCtx{parent, key, val}
}
5.2value
func value(c Context, key any) any {
for {
switch ctx := c.(type) { //对context进行类型断语
case *valueCtx:
if key == ctx.key { //假设key便是当时ctx的key,则直接回来val
return ctx.val
}
c = ctx.Context //不然向父ctx查询
case *cancelCtx:
if key == &cancelCtxKey {
return c
}
c = ctx.Context
case withoutCancelCtx:
if key == &cancelCtxKey {
// This implements Cause(ctx) == nil
// when ctx is created using WithoutCancel.
return nil
}
c = ctx.c
case *timerCtx:
if key == &cancelCtxKey {
return &ctx.cancelCtx
}
c = ctx.Context
case backgroundCtx, todoCtx:
return nil //不存在key value
default:
return c.Value(key) //回来用户完结的Value
}
}
}
能够看见,valueCtx查询value的进程,类似于链表查询,它是自底向上的,而且时刻复杂度为O(N),它并不适用于寄存很多的kv数据,原因有以下:
- 线性时刻复杂度O(N),耗时太长
- 一个 valueCtx 实例只能存一个 kv 对,因而 n 个 kv 对会嵌套 n 个 valueCtx,形成空间糟蹋
- 不支持根据 k 的去重,相同 k 或许重复存在,并根据起点的不同,回来不同的 v. 由此得知,valueContext 的定位类似于恳求头,只合适寄存少数效果域较大的大局 meta 数据.
感谢观看,参阅博客:
Golang context 完结原理 (qq.com)
深化Go:Context-腾讯云开发者社区-腾讯云 (tencent.com)
Go context的运用和源码剖析_&cancelctxkey-CSDN博客