go|context源码解析
文章目录
首先看一下源码对“context”的描述,
When a Context is canceled, all Contexts derived from it are also canceled.
当一个Context被取消时,所有从它派生的Context也会被取消。
The WithCancel, WithDeadline, and WithTimeout functions take a Context (the parent) and return a derived Context (the child) and a CancelFunc. Calling the CancelFunc cancels the child and its children, removes the parent’s reference to the child, and stops any associated timers. Failing to call the CancelFunc leaks the child and its children until the parent is canceled or the timer fires. The go vet tool checks that CancelFuncs are used on all control-flow paths.
WithCancel、WithDeadline和WithTimeout函数接受一个Context(父类)并返回一个派生Context(子类)和一个CancelFunc。调用CancelFunc会取消子context及其由该context派生出的子context,移除父context对子context的引用,并停止所有相关的计时器。未能调用CancelFunc会泄漏子context及其由它派生出的子context,直到父context被取消或计时器触发。go - vet工具检查在所有控制流路径上是否使用了CancelFuncs。
Programs that use Contexts should follow these rules to keep interfaces consistent across packages and enable static analysis tools to check context propagation:
使用上下文的程序应该遵循这些规则,以保持包之间的接口一致,并使静态分析工具能够检查上下文传播:
Do not store Contexts inside a struct type; instead, pass a Context explicitly to each function that needs it. The Context should be the first parameter, typically named ctx:
func DoSomething(ctx context. Context, arg Arg) error {
use ctx
}
不要在结构类型中存储上下文;相反,将上下文显式地传递给需要它的每个函数。Context应该是第一个参数,通常命名为ctx:
func DoSomething(ctx context. Context, arg Arg) error {
use ctx
}
Do not pass a nil Context, even if a function permits it. Pass context.TODO if you are unsure about which Context to use. The same Context may be passed to functions running in different goroutines; Contexts are safe for simultaneous use by multiple goroutines.Use context Values only for request-scoped data that transits processes and APIs, not for passing optional parameters to functions.
即使函数允许也不要传递一个nil_context,如果不知道使用哪一个context可以传递context.todo[其实就是emptyctx]。
相同的context可以被传递在运行在不同协程中的多个函数上.多个goroutine使用同一个context是并发安全的。
context值应该用于在函数和API之间共享的请求范围数据,比如身份验证令牌、请求ID或跟踪信息等数据。,而不是用于传递可选参数。可选参数应该明确地定义为函数参数,以确保代码的清晰性和可维护性。
接下来分析源码
Context接口
type Context interface {Deadline() (deadline time.Time, ok bool)Done() <-chan struct{}Err() errorValue(key any) any
}
可以看出"Context"接口包含了四个方法,接下来依次介绍这四个方法。
Deadline()
Deadline() (deadline time.Time, ok bool)
Deadline()返回当前context应该被取消的时间,如果没有设置取消时间,ok为false。
运行结果如下
0001-01-01 00:00:00 +0000 UTC false
0001-01-01 00:00:00 +0000 UTC false
2023-11-06 15:44:34.580231 +0800 CST m=+5.003172901 true
Done()
Done() <-chan struct{}
Done返回一个只读chan,由于只读所以只有该通道关闭时才会唤醒所有监听该通道的协程,至于何时关闭,有可能时超时关闭,也有可能通过cancelfunc手动关闭。如果无法被关闭返回回一个nil值。对Done()的连续调用会返回相同的值。
Done被用于select语句
func Stream(ctx context.Context, out chan<- Value) error {for {v, err := DoSomething(ctx)if err != nil {return err}select {case <-ctx.Done():return ctx.Err()case out <- v:}}}
Err()
Err() error
- 通道没有被关闭,返回nil
- 通道被关闭返回关闭的原因
如果context被取消返回"Canceled"
如果超时取消会返回"DeadlineExceeded"
//context被cancelfunc取消返回的err
var Canceled = errors.New("context canceled")//context因超时被取消
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 }
Value()
Value(key any) any
Value返回context中与key相关联的value值,没有与之关联的value值返回nil,连读的调用返回相同的结果。
键标识context中的特定值。希望在Context中存储值的函数通常在全局变量中分配一个键,然后将该键用作Context的参数。
由于需要使用"key"进行比较所以需要key是可比较的任何类型。并且建议将键定位为未导出的变量,这样可以避免产生冲突。
定义Context键的包应该为使用该键存储的值提供类型安全的访问器:package user定义了存储在context类型中的value值
package user
import "context"
type User struct {...}
type key int
//未导出的key值,避免冲突;user使用userkey;uservalue存储在context中
var userKey key
//clients use user.NewContext and user.FromContext instead of using this key directly.//NewContext 返回一个携带u值的context
func NewContext(ctx context.Context, u *User) context.Context {return context. WithValue(ctx, userKey, u)
}//FromContext 根据userkey返回value值
func FromContext(ctx context.Context) (*User, bool) {u, ok := ctx.Value(userKey).(*User)return u, ok
}
接下来看看"Context"的具体实现
canceler接口
//canceler是一个可以被直接取消的context 类型
//具体实现是*cancelCtx 和 *timerCtx
type canceler interface {cancel(removeFromParent bool, err, cause error)Done() <-chan struct{}
}
ctx
emptyCtx
/*
emptyCtx永远不会被取消,没有值,也没有截止日期。它不是struct{},因为这种类型的变量必须有不同的地址。[就像C++中的empty class的大小并不是0,因为为了保证类对象的每个实例都有唯一的地址,会在empty class中插入一个变量]
*/
type emptyCtx int
接下来看看empty如何实现Context接口的四种方法
//由于emptyCtx没有设置取消时间故直接返回nil和false
func (*emptyCtx) Deadline() (deadline time.Time, ok bool) {return
}
//emptyCtx不能被取消
func (*emptyCtx) Done() <-chan struct{} {return nil
}func (*emptyCtx) Err() error {return nil
}
//emptyCtx不能存储值
func (*emptyCtx) Value(key any) any {return nil
}
emptyCtx有两个具体的变量
var (background = new(emptyCtx)todo = new(emptyCtx)
)
func (e *emptyCtx) String() string {switch e {case background:return "context.Background"case todo:return "context.TODO"}return "unknown empty Context"
}
/*
Background返回一个非空的empty Context。它永远不会被取消,没有值,也没有截止日期。它通常由main函数、初始化和测试使用,并作为传入请求的根context。
*/
func Background() Context {return background
}
/*
TODO返回一个非空的empty Context。当不清楚使用哪个context或context还不可用时(因为周围的函数还没有扩展到接受上下文参数)使用todo
*/
func TODO() Context {return todo
}
cancelCtx
首先看一下cancelCtx的结构定义
//cancelCtx可以被取消。当被取消的时候,也会取消实现了
//canceler接口的chidlen context
type cancelCtx struct {Context //组合继承Context接口mu sync.Mutex // protects following fieldsdone atomic.Value // of chan struct{}, created lazily, closed by first cancel call 用来懒汉式创造chan,被关闭通过第一次的cancel调用children map[canceler]struct{} // set to nil by the first cancel callerr error // set to non-nil by the first cancel callcause error // set to non-nil by the first cancel call
}
看一下cancelFunc的定义
//CancelFun不会等work停止,会强制终止当前的work
//可以被多个写成同时调用
//在第一次调用之后,之后的调用CancelFunc什么也不做
type CancelFunc func()
看一下如何获取cancelCtx
/*
WithCancel返回一个副本:父context和一个新的Done通道。
返回的context通道被关闭的时机:
1.调用返回的cancelfunc
2.父context的Done chan被关闭。
取消context会释放它所关联的资源,所以运行在该context的操作完成之后应该调用cancelfunc释放资源
*/
func WithCancel(parent Context) (ctx Context, cancel CancelFunc) {c := withCancel(parent)return c, func() { c.cancel(true, Canceled, nil) }
}
func withCancel(parent Context) *cancelCtx {if parent == nil {// parent context不能为nilpanic("cannot create context from nil parent")}// 将c的Context设置为parentc := newCancelCtx(parent)// 找到parent中的cancelCtx// 将c加入到该cancelCtx的child列表propagateCancel(parent, c)return c
}
// newCancelCtx returns an initialized cancelCtx.
func newCancelCtx(parent Context) *cancelCtx {return &cancelCtx{Context: parent}
}
//从这里也可以看出WithCancel(parent ...)返回的是一个带有parent context和new done chan的副本。
接下来看一下withcancel的另外一个函数“propagateCancel”
[propagate传播]
// propagateCancel arranges for child to be canceled when parent is.
//这个函数的作用是将child节点加入到parent节点的childen中
func propagateCancel(parent Context, child canceler) {//获取parent的Done()done := parent.Done()//父context不能被取消,什么也不做if done == nil {return // parent is never canceled}select {case <-done://父context已经被取消,直接取消子context// parent is already canceledchild.cancel(false, parent.Err(), Cause(parent))returndefault://放行执行下面的逻辑}//在parent中找到了*cancelCtxif p, ok := parentCancelCtx(parent); ok {p.mu.Lock()if p.err != nil {// parent has already been canceledchild.cancel(false, p.err, p.cause)} else {if p.children == nil {// 懒惰式创建p.children = make(map[canceler]struct{})}//将child加入到cancelCtx的children中p.children[child] = struct{}{}}p.mu.Unlock()} else {/*// goroutines counts the number of goroutines ever created; 记录创建的协程数量var goroutines atomic.Int32*/goroutines.Add(1)//新开启一个协程监听parent.Done()和child.Done()go func() {select {case <-parent.Done():child.cancel(false, parent.Err(), Cause(parent))case <-child.Done():}}()}
}
/*
parentCancelCtx返回父对象的底层*cancelCtx。它通过查找
parent.Value(&cancelCtxKey)来找到最里面的封闭
*cancelCtx,然后检查parent.Done()是否与*cancelCtx匹配。
(如果没有,*cancelCtx已经被封装在提供不同done通道的自定义实
现中,在这种情况下我们不应该绕过它。)
*/
//&cancelCtxKey is the key that a cancelCtx returns itself for.
var cancelCtxKey int
func parentCancelCtx(parent Context) (*cancelCtx, bool) {//获取parent的contextdone := parent.Done()/*closedchan是一个可重用的已经被关闭的通道var closedchan = make(chan struct{})func init() {close(closedchan)}*/if done == closedchan || done == nil {return nil, false}//在parent中找到cancelCtxKey对应的value值p, ok := parent.Value(&cancelCtxKey).(*cancelCtx)if !ok {//没有找到return nil, false}//加载从父context中找到的cancelCtx的chanpdone, _ := p.done.Load().(chan struct{})//从父context中寻找到的cancelCtx的Done()chan与从parent中的Done()获取到的chan不是同一个,说明parent自定义了Done()方法if pdone != done {return nil, false}return p, true
}
看一下cancelCtx对Context接口的实现
cancelCtx并没有实现Deadline方法,所以是继承了parent ctx的Deadline()方法
func (c *cancelCtx) Value(key any) any {if key == &cancelCtxKey {return c}return value(c.Context, key)
}
func value(c Context, key any) any {for {//c一定要是一个interface才可以使用c.(type)switch ctx := c.(type) {case *valueCtx:// valueCtx返回对应的val值if key == ctx.key {return ctx.val}c = ctx.Contextcase *cancelCtx:// cancelCtx返回自身if key == &cancelCtxKey {return c}c = ctx.Contextcase *timerCtx:// timerCtx返回其中的cancelCtxif key == &cancelCtxKey {return ctx.cancelCtx}c = ctx.Contextcase *emptyCtx:return nildefault://自定义的Value方法return c.Value(key)}}
}
//双重检测创建chan
func (c *cancelCtx) Done() <-chan struct{} {//c.done是一个atomic.Value类型的变量,是对结构体类型的原子操作,是并发安全的//获取c中存储的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.errc.mu.Unlock()return err
}
// cancel sets c.cause to cause if this is the first time c is canceled.
//cancel 调用关闭c.done,取消每一个c的children,
//如果removeFromParent是true,将c从parent的children中移除。
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已经被取消过c.mu.Unlock()return // already canceled}//设置c.errc.err = errc.cause = cause//获取c中的chand, _ := c.done.Load().(chan struct{})if d == nil {//closedchan是一个可重用的已经被关闭的通道c.done.Store(closedchan)} else {//closeclose(d)}//遍历c.children关闭由c派生出的cancelCtxfor child := range c.children {// NOTE: acquiring the child's lock while holding parent's lock.// removeFromParent为true,需要获取父节点的锁,但此时已经持有着父节点的锁,所以为falsechild.cancel(false, err, cause)}c.children = nilc.mu.Unlock()//从父context中的children中移除cif removeFromParent {removeChild(c.Context, c)}
}
// removeChild removes a context from its parent.
func removeChild(parent Context, child canceler) {p, ok := parentCancelCtx(parent)if !ok {return}p.mu.Lock()if p.children != nil {//将child从parent的children中移除delete(p.children, child)}p.mu.Unlock()
}
timerCtx
首先看一下timerCtx的定义
/*timerCtx带有一个计时器和一个截止日期。它嵌入了一个 cancelCtx来实现Done和Err。它通过停止计时器,然后委托给cancelCtx.cancel来实现取消。
*/
type timerCtx struct {*cancelCtxtimer *time.Timer // 定时器deadline time.Time //到期时间
}
timerCtx组合继承了cancelCtx,由cancelCtx实现Done()和Err()方法,Deadline()和cancel()方法由timerCtx自己实现
func (c *timerCtx) Deadline() (deadline time.Time, ok bool) {return c.deadline, true
}
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()
看看获取*timerCtx的方法"withDeadline"
/*withDeadline返回一个带有parent context和截止时间deadline的副本;如果parent context的deadline早于d,WithDeadline(parent,d)等同于WithCancel(parent) ;返回的context chan被关闭的时机:1.到截止时间2.cancelfunc被调用3.parent chan被关闭。取消上下文会释放与之关联的资源,所以执行完相关的操作之后要调用cancelfunc释放资源*/
func WithDeadline(parent Context, d time.Time) (Context, CancelFunc) {if parent == nil {panic("cannot create context from nil parent")}if cur, ok := parent.Deadline(); ok && cur.Before(d) {// parent的过期时间早于child,等价于cancelCtxreturn WithCancel(parent)}c := &timerCtx{cancelCtx: newCancelCtx(parent),deadline: d,}// 找到parent中的cancelCtx// 将c加入到该cancelCtx的child列表propagateCancel(parent, c)//当前时间距离d的间隙dur := time.Until(d)if dur <= 0 {//到期取消c.cancel(true, DeadlineExceeded, nil) // deadline has already passedreturn c, func() { c.cancel(false, Canceled, nil) }}//设置定时器c.mu.Lock()defer c.mu.Unlock()if c.err == nil {//到达截止时间dur之后调用自定义的functionc.timer = time.AfterFunc(dur, func() {c.cancel(true, DeadlineExceeded, nil)})}return c, func() { c.cancel(true, Canceled, nil) }
}
另一个创建*timerCtx的方式"withTimeout"
/*
取消context会释放与它相关的资源,所以代码应该
在此context运行的操作完成后立即调用cancel:
func slowOperationWithTimeout(ctx context.Context) (Result, error) {ctx, cancel := context.WithTimeout(ctx, 100*time.Millisecond)defer cancel() // releases resources if slowOperation completes before timeout elapsesreturn slowOperation(ctx)
}
*/
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc) {return WithDeadline(parent, time.Now().Add(timeout))
}
valueCtx
先看一下valueCtx的定义
//valueCtx带有key-value pair,可以存储值但不能被取消;
//只实现自己Value方法,其它的方法由Context 接口实现
type valueCtx struct {Contextkey, val any
}
创建*valueCtx的方法"WithValue"
/*
提供的key必须是可比较的,并且不应该是string或者任何其它的内置类型这样可以在使用context的package之间避免冲突。使用WithValue必须定义自己的key类型。
为了避免将key赋值给interface的时候产生额外的分配,应该将key设置为具体的struct或者具体的内置类型;如果想将key设置为可导出[exported]的变量,那么key的静态数据类型应该是指针或者接口类型的,这样可以避免在上下文传递是引起不必要的拷贝。example:
var MyKey *string
var MyKey interface{}
*/
func WithValue(parent Context, key, val any) Context {if parent == nil {panic("cannot create context from nil parent")}if key == nil {panic("nil key")}//key一定要是可比较的类型//go中不可比较的类型有三个:slice,map,functionif !reflectlite.TypeOf(key).Comparable() {panic("key is not comparable")}return &valueCtx{parent, key, val}
}
*valueCtx实现的Value方法
func (c *valueCtx) Value(key any) any {if c.key == key {return c.val}return value(c.Context, key)
}func value(c Context, key any) any {for {switch ctx := c.(type) {case *valueCtx:if key == ctx.key {return ctx.val}c = ctx.Contextcase *cancelCtx:if key == &cancelCtxKey {return c}c = ctx.Contextcase *timerCtx:if key == &cancelCtxKey {return ctx.cancelCtx}c = ctx.Contextcase *emptyCtx:return nildefault:return c.Value(key)}}
}
以上有关context的源码就分析结束了,看一下各个Ctx的应用
基本使用
cancelCtx
func main() {get := func(ctx context.Context) <-chan int {ch := make(chan int)n := 1go func() {for {select {case <-ctx.Done():returncase ch <- n:n++//default: //不要有这个分支不然这个子goroutine会一直for轮询直到被抢占调度}}}()return ch}ctx, cancel := context.WithCancel(context.Background())defer cancel()//防止goroutine泄露for v := range get(ctx) {fmt.Println(v)if v == 5 {break}}
}
valueCtx
type keytype string
var key keytypefunc main(){//WithValue的example,自定义具体的key类型f := func(ctx context.Context, key keytype) {if v := ctx.Value(key); v != nil {fmt.Println("find key ,value", v)} else {fmt.Println("not find key,", key)}}ctx1 := context.WithValue(context.Background(), key, "GO")f(ctx1, key)//find key ,value GOf(ctx1, keytype("key"))//not find key, key
}
/*
CancelFuncCause的作用等同于CancelFunc,但是添加了额外的取消原因[cancellation cause],如果context已经被取消不会设置cause
*/
// For example, if childContext is derived from parentContext:
// - if parentContext is canceled with cause1 before childContext is canceled with cause2,
// then Cause(parentContext) == Cause(childContext) == cause1
// - if childContext is canceled with cause2 before parentContext is canceled with cause1,
// then Cause(parentContext) == cause1 and Cause(childContext) == cause2
type CancelCauseFunc func(cause error)/*
WithCancelCause 的行为等同于WithCancel,但是返回的是CancelFuncCause而不是CancelFunc。调用CancelFuncCause并传入一个非nil的error,可以通过Cause(ctx)进行检索。如果调用CancelFuncCause时传入nil,cause会被设置为Canceled与err记录的值一样
*/
func WithCancelCause(parent Context) (ctx Context, cancel CancelCauseFunc) {c := withCancel(parent)return c, func(cause error) { c.cancel(true, Canceled, cause) }
}
使用例子
ctx, cancel := context.WithCancelCause(parent)
cancel(myError)
ctx.Err() // returns context.Canceled
context.Cause(ctx) // returns myError