go 进阶 八. 标准库之context

目录

  • 一. Context 初识
    • 内部方法解释
      • 1. Deadline() (deadline time.Time, ok bool)
      • 2. Done() <-chan struct{}
      • 3. Done() <-chan struct{}
      • 4. Err() error
      • 5. Value(key interface{}) interface{}
  • 二. go默认提供的context
    • 空context emptyCtx
    • cancelCtx 被cancle时会把其中的所有child都cancle掉
      • 1. Done()接口实现
      • 2. Err()接口实现
      • 3. cancel()接口实现
      • 4. WithCancel()方法实现
    • timerCtx
      • 1. Deadline()接口实现
      • 2. cancel()接口实现
      • 3. WithDeadline()方法实现
      • 4. WithTimeout()方法实现
      • 5. 典型使用案例
  • 三. 再次解释context作用
    • 基于 context 派生的 context 的标准方法
      • 1. WithCancel() 生成一个带取消函数的子上下文
      • 2. WithDeadline() 与 WithTimeOut() 接收过期时间,生成一个带取消函数的子上下文
    • 官方对context使用建议
    • 使用context传递共享数据示例
    • 使用context 取消 goroutine 示例
    • 使用context防止 goroutine 泄漏
    • 超时取消示例
  • 四. context.Value 的查找过程
    • 底层结构
    • valueCtx 的创建
    • 获取
      • 注意点
  • 五. 问题及总结
    • context的缺陷及注意点
    • 什么是Context
    • context.Value 的查找过程

一. Context 初识

  1. context是Golang应用开发常用的并发控制技术,它与WaitGroup最大的不同点是context对于派生goroutine有更强的控制力,它可以控制多级的goroutine

假设goroutine派生出子goroutine,子goroutine又继续派生新的,这种情况下使用WaitGroup比较麻烦,因为子goroutine个数不容易确定,而使用context就可以很容易实现

  1. context实际上只定义了接口,凡是实现该接口的类都可称为是一种context, 底层结构:
type Context interface {
    Deadline() (deadline time.Time, ok bool)
    Done() <-chan struct{}
    Err() error
    Value(key interface{}) interface{}
}

内部方法解释

1. Deadline() (deadline time.Time, ok bool)

该方法获取设置的截止时间, deadline 为截止时间, 到了这个时间 ctx 会自动发起取消请求. 如果没有设置截止时间, 那么需要手动调用 cancel() 方法来停止. ok==false时表示没有设置截止时间

2. Done() <-chan struct{}

该方法返回一个只读的 channel 类型是 struct{}, 需要在select-case语句中使用,如”case <-context.Done():”当有信号时,表明parent context 已经发起了取消, goroutine 中通过 Done chan 获取到取消信号后, 应当做清理操作,然后退出协程,释放资源

3. Done() <-chan struct{}

  1. 该方法返回一个只读的 channel 类型是 struct{}, 需要在select-case语句中使用,如”case <-context.Done():”
  2. 当context关闭后,Done()返回一个被关闭的管道,关闭的管理仍然是可读的,据此goroutine可以收到关闭请求;当context还未关闭时,Done()返回nil。
  3. goroutine 中可以通过 Done chan 获取到取消信号后, 应当做清理操作,然后退出协程,释放资源

4. Err() error

  1. 返回 ctx 为什么被取消, 关闭原因由context实现控制,不需要用户设置,比如Deadline context,关闭原因可能是因为deadline,也可能提前被主动关闭,那么关闭原因就会不同:
  1. 因deadline关闭:“context deadline exceeded”;
  2. 因主动关闭: “context canceled”。
  1. 当context关闭后,Err()返回context的关闭原因;当context还未关闭时,Err()返回nil;

5. Value(key interface{}) interface{}

  1. 获取 ctx 上绑定的值, 通常线程安全
  2. 有一种context,它不是用于控制呈树状分布的goroutine,而是用于在树状分布的goroutine间传递信息,Value()方法就是用于此种类型的context,该方法根据key值查询map中的value

二. go默认提供的context

  1. context包中实现Context接口的struct,有 emptyCtx, cancelCtx、timerCtx和valueCtx,正是基于这三种context实例
  2. 各context类型之间的关系
    go 进阶 八. 标准库之context_第1张图片
  3. 其中 emptyCtx一个空context,另外还有cancelCtx、valueCtx、valueCtx都继承于Context,三者都返回一个可取消的 context 实例,和取消函数

空context emptyCtx

  1. context包中定义了一个空的context, 名为emptyCtx,用于context的根节点,空的context只是简单的实现了Context,本身不包含任何值,仅用于其他context的父节,源码:
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. context包中定义了一个公用的emptCtx全局变量,名为background,可以使用context.Background()获取空emptCtx
var background = new(emptyCtx)
func Background() Context {
    return background
}

cancelCtx 被cancle时会把其中的所有child都cancle掉

  1. cancelCtx 源码, 内部包含一个 children属性,记录了由此context派生的所有child,此context被cancle时会把其中的所有child都cancle掉
type cancelCtx struct {
    Context
    mu       sync.Mutex            // protects following fields
    done     chan struct{}         // created lazily, closed by first cancel call
    children map[canceler]struct{} // 维护了所有的衍生节点
    err      error                 // set to non-nil by the first cancel call
}
  1. cancelCtx与deadline和value无关,所以只需要实现Done()和Err()接口外露接口即可。

1. Done()接口实现

  1. 按照Context定义,Done()接口只需要返回一个channel即可,对于cancelCtx来说只需要返回成员变量done即可
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
}
  1. cancelCtx没有指定初始化函数,所以cancelCtx.done可能还未分配,所以需要考虑初始化。cancelCtx.done会在context被cancel时关闭,所以cancelCtx.done的值一般经历如三个阶段:nil —> chan struct{} —> closed chan

2. Err()接口实现

  1. 按照Context定义,Err()只需要返回一个error告知context被关闭的原因。对于cancelCtx来说只需要返回成员变量err即可
func (c *cancelCtx) Err() error {
    c.mu.Lock()
    err := c.err
    c.mu.Unlock()
    return err
}
  1. cancelCtx.err默认是nil,在context被cancel时指定一个error变量: var Canceled = errors.New(“context canceled”)

3. cancel()接口实现

  1. cancel()内部方法是理解cancelCtx的最关键的方法,其作用是关闭自己和其后代,其后代存储在cancelCtx.children的map中,其中key值即后代对象,value值并没有意义,这里使用map只是为了方便查询而已
  2. 伪代码如下
func (c *cancelCtx) cancel(removeFromParent bool, err error) {
    c.mu.Lock()
    c.err = err                          //设置一个error,说明关闭原因
    close(c.done)                     //将channel关闭,以此通知派生的context
    for child := range c.children {   //遍历所有children,逐个调用cancel方法
        child.cancel(false, err)
    }
    c.children = nil
    c.mu.Unlock()
    if removeFromParent {            //正常情况下,需要将自己从parent删除
        removeChild(c.Context, c)
    }
}
  1. 实际上,WithCancel()返回的第二个用于cancel context的方法正是此cancel()

4. WithCancel()方法实现

  1. WithCancel()方法作了三件事:
  1. 初始化一个cancelCtx实例
  2. 将cancelCtx实例添加到其父节点的children中(如果父节点也可以被cancel的话)
  3. 返回cancelCtx实例和cancel()方法
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) {
    ...
    if p, ok := parentCancelCtx(parent); ok {
        // 当 child 的继承链包含可以取消的上下文时,会判断 parent 是否已经触发了取消信号;
        p.mu.Lock()
        if p.err != nil {
            // 如果已经被取消,child 会立刻被取消;
            child.cancel(false, p.err)
        } else {
            if p.children == nil {
                p.children = make(map[canceler]struct{})
            }
            // 如果没有被取消,child 会被加入 parent 的 children 列表中,等待 parent 释放取消信号;
            p.children[child] = struct{}{}
        }
        p.mu.Unlock()
    } else {
        atomic.AddInt32(&goroutines, +1)
        // 运行一个新的 Goroutine 同时监听 parent.Done() 和 child.Done() 两个 Channel
        // 在 parent.Done() 关闭时调用 child.cancel 取消子上下文;
        go func() {
            select {
            case <-parent.Done():
                child.cancel(false, parent.Err())
            case <-child.Done():
            }
        }()
    }
}
  1. 查看源码发现: 在调用链的过程中,会调用一个goroutine同时监听parent.Done()和child.Done(),当有parent.Done()关闭的时候就会调用child.cancel的方法来取消上下文,同时会在构造树上将该child节点删掉,删掉此child节点之后,还会轮训的将此child节点的child节点全部都调用cancel函数使其退出,最终实现了此parent节点的全部孩子节点都退出
  2. 这里将自身添加到父节点的过程有必要简单说明一下:
  1. 如果父节点也支持cancel,也就是说其父节点肯定有children成员,那么把新context添加到children里即可;
  2. 如果父节点不支持cancel,就继续向上查询,直到找到一个支持cancel的节点,把新context添加到children里;
  3. 如果所有的父节点均不支持cancel,则启动一个协程等待父节点结束,然后再把当前context结束

timerCtx

  1. timerCtx在cancelCtx基础上增加了deadline用于标示自动cancel的最终时间,而timer就是一个触发自动cancel的定时器
//src/context/context.go:timerCtx
type timerCtx struct {
    cancelCtx
    timer *time.Timer // Under cancelCtx.mu.
    deadline time.Time
}
  1. 由此,衍生出WithDeadline()和WithTimeout()。实现上这两种类型实现原理一样,只不过使用语境不一样:
  1. deadline: 指定最后期限,比如context将2018.10.20 00:00:00之时自动结束
  2. timeout: 指定最长存活时间,比如context将在30s后结束
  1. 对于接口来说,timerCtx在cancelCtx基础上还需要实现Deadline()和cancel()方法,其中cancel()方法是重写的

1. Deadline()接口实现

Deadline()方法仅仅是返回timerCtx.deadline而矣。而timerCtx.deadline是WithDeadline()或WithTimeout()方法设置的

2. cancel()接口实现

  1. cancel()方法基本继承cancelCtx,只需要额外把timer关闭。
  2. timerCtx被关闭后,timerCtx.cancelCtx.err将会存储关闭原因:
  1. 如果deadline到来之前手动关闭,则关闭原因与cancelCtx显示一致;
  2. 如果deadline到来时自动关闭,则原因为:”context deadline exceeded”

3. WithDeadline()方法实现

  1. WithDeadline()方法实现步骤如下:
  1. 初始化一个timerCtx实例
  2. 将timerCtx实例添加到其父节点的children中(如果父节点也可以被cancel的话)
  3. 启动定时器,定时器到期后会自动cancel本context
  4. 返回timerCtx实例和cancel()方法
  1. 也就是说,timerCtx类型的context不仅支持手动cancel,也会在定时器到来后自动cancel

4. WithTimeout()方法实现

WithTimeout()实际调用了WithDeadline,二者实现原理一致,源码

func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc) {
    return WithDeadline(parent, time.Now().Add(timeout))
}

5. 典型使用案例

  1. 例子中使用WithTimeout()获得一个context并在其了协程中传递
package main
import (
    "fmt"
    "time"
    "context"
)
func HandelRequest(ctx context.Context) {
    go WriteRedis(ctx)
    go WriteDatabase(ctx)
    for {
        select {
        case <-ctx.Done():
            fmt.Println("HandelRequest Done.")
            return
        default:
            fmt.Println("HandelRequest running")
            time.Sleep(2 * time.Second)
        }
    }
}
func WriteRedis(ctx context.Context) {
    for {
        select {
        case <-ctx.Done():
            fmt.Println("WriteRedis Done.")
            return
        default:
            fmt.Println("WriteRedis running")
            time.Sleep(2 * time.Second)
        }
    }
}
func WriteDatabase(ctx context.Context) {
    for {
        select {
        case <-ctx.Done():
            fmt.Println("WriteDatabase Done.")
            return
        default:
            fmt.Println("WriteDatabase running")
            time.Sleep(2 * time.Second)
        }
    }
}
func main() {
    ctx, _ := context.WithTimeout(context.Background(), 5 * time.Second)
    go HandelRequest(ctx)
    time.Sleep(10 * time.Second)
}

三. 再次解释context作用

  1. context是Go 1.7 标准库引入的, 主要用来在 goroutine 之间传递上下文信息,包括:取消信号、超时时间、截止时间、k-v 等, context 几乎成为了并发控制和超时控制的标准做法, 还有一点: 在Go 里,我们不能直接杀死协程,协程的关闭一般会用 channel+select 方式来控制。但是在某些场景下,例如处理一个请求衍生了很多协程,这些协程之间是相互关联的:需要共享一些全局变量、有共同的 deadline 等,而且可以同时被关闭。再用 channel+select 就会比较麻烦,这时就可以通过 context 来实现, 总结一句话: context 用来解决 goroutine 之间退出通知、元数据传递的功能
  2. 例如,当接收到请求时,你需要使用多个子协程去处理数据,但是此时业务报错,你需要去取消子协程并对请求做返回,直接返回不管子协程就有可能造成脏数据,而且子协程可能占用系统资源,所以你是需要关闭子协程的。而context就可以提供当子协程正在运行时,父协程可以关闭子协程这个功能。
  3. 通过上面我们知道可以调用获取一个空的context,作为根节点
func Background() Context
  1. 拿到跟节点后, 基于 context 派生的 context 的标准方法,就可以简单的实现父协程管理子协程的功能

context的呈现的形式像二叉树结构,有父子关系,父协程管理子协程。context的使用场景就是主协程管理多个子协程,这边的管理就是简单粗暴的关闭子协程

基于 context 派生的 context 的标准方法

//生成一个带取消函数cancelFunc的子上下文ctx(通过调用返回的cancelFunc,释放一个信号,此时从ctx.Done()这个channel就可以获取到取消的信号)
func WithCancel(parent Context) (ctx Context, cancel CancelFunc)
func WithDeadline(parent Context, d time.Time) (Context, CancelFunc)
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc)

1. WithCancel() 生成一个带取消函数的子上下文

  1. 使用示例
package main

import (
	"context"
	"fmt"
	"time"
)

func dosomething(ctx context.Context) {
	for {
		select {
		case <-ctx.Done():
			fmt.Println("playing")
			return
		default:
			fmt.Println("I am working!")
			time.Sleep(time.Second)
		}
	}
}

func main() {
	ctx, cancelFunc := context.WithCancel(context.Background())
	go func() {
		time.Sleep(5 * time.Second)
		cancelFunc()
	}()
	dosomething(ctx)
}
  1. 查看该函数底层源码
func WithCancel(parent Context) (ctx Context, cancel CancelFunc) {
	if parent == nil {
		panic("cannot create context from nil parent")
	}
	c := newCancelCtx(parent)
	propagateCancel(parent, &c)
	return &c, func() { c.cancel(true, Canceled) }
}

// newCancelCtx returns an initialized cancelCtx.
func newCancelCtx(parent Context) cancelCtx {
	return cancelCtx{Context: parent}
}
  1. 实际会返回一个cancelCtx 结构变量,内部包含一个done属性,是一个传递空结构体类型的channel,用来在上下文取消时关闭这个通道,err就是在上下文被取消时告诉用户这个上下文取消了,可以用ctx.Err()来获取信息
type cancelCtx struct {
	Context

	mu       sync.Mutex            // protects following fields
	done     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
}
  1. 查看cancel(),该方法取消了基于该上下文的所有子上下文以及把自身从父上下文中取消
func (c *cancelCtx) cancel(removeFromParent bool, err error) {
	...
	if c.done == nil {
		c.done = closedchan
	} else {
		close(c.done)
	}
	...
}
  1. 查看该绑定的Done()方法,cancel函数将c.done通道关闭了,从一个被close的channel中接收数据不会被阻塞,而是立即返回,接收完已发送的数据后会返回传递的元素类型的零值(zero value),从而可以从ctx.Done()获取返回值{},最后结束该上下文
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
}

2. WithDeadline() 与 WithTimeOut() 接收过期时间,生成一个带取消函数的子上下文

  1. 利用根Context创建一个父Context,使用父Context创建2个协程,设定3秒过期
  2. 等待5秒钟,再调用cancel函数,其实这个时候会发现在3秒钟的时候两个协程已经收到退出信号了
  3. WithTimeOut和WithDeadline作用和效果类似,只是传递的时间参数不同,在调用WithTimeout时,内部调用的是WithDeadline,实例化timeCtx,而它的父类是cancelCtx. 使用time.AfterFunc,在一定时间后关闭通道,达到一定时间后通过管道通信,来关闭函数
  4. WithDeadline 底层代码
func WithDeadline(parent Context, d time.Time) (Context, CancelFunc) {
	if cur, ok := parent.Deadline(); ok && cur.Before(d) {
		//时间过了 cancelCtx
		return WithCancel(parent)
	}
	//实例timeCtx
	c := &timeCtx{
		//cancel的上下文
		cancelCtx: newCancelCtx(parent),
		deadline:  d,
	}
 
	//cancelCtx也调用了这个函数
	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. WithTimeout底层代码
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc) {
	//调用WithDeadline, 当前时间+timeout,时间到了 done管道收到信息,函数自行关闭
	return WithDeadline(parent, time.Now().Add(timeout))
}
  1. timeCtx 的cancel实现
func (c *timeCtx) cancel(removeFromParent bool, err error) {
	//调用cancelCtx的cancel进行关闭
	c.cancelCtx.cancel(false, err)
	if removeFromParent {
		removeChild(c.cancelCtx.Context, c)
	}
	c.mu.Lock()
	defer c.mu.Unlock()
	//关闭
	if c.timer != nil {
		c.timer.Stop()
		c.timer = nil
	}
 
}

官方对context使用建议

  1. 避免Context 塞到结构体中,推荐直接将 Context 类型作为函数的第一参数,命名为 ctx
  2. 避免像函数传递一个为nil 的 context,如果确实没有或者不使用,标准库准备了一个 emptyCtx
  3. 避免将函数需要的参数设置到context 中,通过context传递,context存储的应该是一些共享数据,比如登陆的session,cookie 等
  4. context 是并发安全的,同一个 context 可以被传递到多个 goroutine

使用context传递共享数据示例

  1. 示例一:
package main
import (
    "context"
    "fmt"
)
func main() {
	//1.获取一个空context
    ctx := context.Background()
    process(ctx)
    //2.向context写数据
    ctx = context.WithValue(ctx, "traceId", "qcrao-2019")
    process(ctx)
}
func process(ctx context.Context) {
	//2.读取context数据
    traceId, ok := ctx.Value("traceId").(string)
    if ok {
        fmt.Printf("process over. trace_id=%s\n", traceId)
    } else {
        fmt.Printf("process over. no trace_id\n")
    }
}
  1. 示例二: 从一个 HTTP 请求中获取到的 Request-ID
const requestIDKey int = 0
func WithRequestID(next http.Handler) http.Handler {
    return http.HandlerFunc(
        func(rw http.ResponseWriter, req *http.Request) {
            // 从 header 中提取 request-id
            reqID := req.Header.Get("X-Request-ID")
            // 创建 valueCtx。使用自定义的类型,不容易冲突
            ctx := context.WithValue(
                req.Context(), requestIDKey, reqID)
            // 创建新的请求
            req = req.WithContext(ctx)
            // 调用 HTTP 处理函数
            next.ServeHTTP(rw, req)
        }
    )
}
// 获取 request-id
func GetRequestID(ctx context.Context) string {
    ctx.Value(requestIDKey).(string)
}
func Handle(rw http.ResponseWriter, req *http.Request) {
    // 拿到 reqId,后面可以记录日志等等
    reqID := GetRequestID(req.Context())
    ...
}
func main() {
    handler := WithRequestID(http.HandlerFunc(Handle))
    http.ListenAndServe("/", handler)
}

使用context 取消 goroutine 示例

  1. 需求: 每隔指定时间后,调用calculatePos()计算一个数据结果,然后调用sendResult()将结果数据发送给需求方
  2. 不使用context实现:
func Perform() {
    for {
    	//计算获取结果
        calculatePos()
        //发送
        sendResult()
        //延迟
        time.Sleep(time.Second)
    }
}
  1. 问题: 假设后续需要实现“取消”功能,可能要给函数增加一个指针型的 bool 变量,在 for 语句的开始处判断 bool 变量是发由 true 变为 false,如果改变,则退出循环,如果内部比较复杂,就会出现各种嵌套,就会很麻烦,进而引出context
  2. 使用context实现示例
func Perform(ctx context.Context) {
    for {
        calculatePos()
        sendResult()
        select {
        case <-ctx.Done():
            // 通过<-ctx.Done()监听取消状态,被取消时直接返回
            return
        case <-time.After(time.Second):
            // block 1 秒钟 
        }
    }
}
  1. 主流程
//1.调用WithTimeout()创建一个ctx,返回ctx 与取消函数cancel
ctx, cancel := context.WithTimeout(context.Background(), time.Hour)
//2.调用业务接口
go Perform(ctx)

//3.当判断需要取消时,直接再此处取消即可
cancel()
  1. 上方为什么使用WithTimeout()创建context:

WithTimeOut 函数返回的 context 和 cancelFun 是分开的。context 本身并没有取消函数,这样做的原因是取消函数只能由外层函数调用,防止子节点 context 调用取消函数,从而严格控制信息的流向:由父节点 context 流向子节点 context

使用context防止 goroutine 泄漏

  1. 一个不用context取消goroutine泄漏的例子
func gen() <-chan int {
    ch := make(chan int)
    go func() {
        var n int
        for {
            ch <- n
            n++
            time.Sleep(time.Second)
        }
    }()
    return ch
}

//如果只需要它产生的前 5 个数,那么就会发生 goroutine 泄漏
func main() {
    for n := range gen() {
        fmt.Println(n)
        if n == 5 {
            break
        }
    }
    // ……
}
  1. 用 context 改进
func gen(ctx context.Context) <-chan int {
    ch := make(chan int)
    go func() {
        var n int
        for {
            select {
            case <-ctx.Done():
                return
            case ch <- n:
                n++
                time.Sleep(time.Second)
            }
        }
    }()
    return ch
}
func main() {
    ctx, cancel := context.WithCancel(context.Background())
    defer cancel() // 避免其他地方忘记 cancel,且重复调用不影响
    for n := range gen(ctx) {
        fmt.Println(n)
        if n == 5 {
            cancel()
            break
        }
    }
    // ……
}

超时取消示例

  1. 程序在对外部服务进行HTTP API调用时设置超时时间。如果被调用服务花费的时间太长,到时间后就会取消请求

或者通过select监听context的ctx.Done()当取消时此处会拿到消息,可以进行指定处理

func main() {
    // 创建一个超时时间为100毫秒的上下文
    ctx := context.Background()
    ctx, _ = context.WithTimeout(ctx, 100*time.Millisecond)
 
    // 创建一个访问Google主页的请求
    req, _ := http.NewRequest(http.MethodGet, "http://google.com", nil)
    // 将超时上下文关联到创建的请求上
    req = req.WithContext(ctx)
 
    // 创建一个HTTP客户端并执行请求
    client := &http.Client{}
    res, err := client.Do(req)
    // 如果请求失败了,记录到STDOUT
    if err != nil {
        fmt.Println("Request failed:", err)
        return
    }
    // 请求成功后打印状态码
    fmt.Println("Response received, status code:", res.StatusCode)
}

四. context.Value 的查找过程

底层结构

  1. 先看一下底层当执行context.Value时会返回一个valueCtx 结构,该结构继承了Context,它仍然是一个 Context,并且绑定了一个String()和Value()方法
type valueCtx struct {
    Context
    key, val interface{}
}

func (c *valueCtx) String() string {
    return fmt.Sprintf("%v.WithValue(%#v, %#v)", c.Context, c.key, c.val)
}
func (c *valueCtx) Value(key interface{}) interface{} {
    if c.key == key {
        return c.val
    }
    return c.Context.Value(key)
}

valueCtx 的创建

  1. 在创建是底层会执行WithValue()
func WithValue(parent Context, key, val interface{}) Context {
    if key == nil {
        panic("nil key")
    }
    if !reflect.TypeOf(key).Comparable() {
        panic("key is not comparable")
    }
    return &valueCtx{parent, key, val}
}
  1. WithValue 创建 context 节点的过程实际上就是创建链表节点的过程。两个节点的 key 值是可以相等的,但它们是两个不同的 context 节点。查找的时候,会向上查找到最后一个挂载的 context 节点,也就是离得比较近的一个父节点 context。所以,整体上而言,用 WithValue 构造的其实是一个低效率的链表

获取

  1. 在创建Context时,由于父管理子, 最终多个Context会形成一棵树
    go 进阶 八. 标准库之context_第2张图片
  2. 取值的过程,实际上是一个递归查找的过程, 会顺着链路一直往上找,比较当前节点的 key是否是要找的 key,如果是,则直接返回 value。否则,一直顺着 context 往前,最终找到根节点(一般是 emptyCtx),直接返回一个 nil。所以用 Value 方法的时候要判断结果是否为 nil
func (c *valueCtx) Value(key interface{}) interface{} {
    if c.key == key {
        return c.val
    }
    return c.Context.Value(key)
}

注意点

  1. 可能会出现这种代码:在一个处理过程中,有若干子函数,子协程。各种不同的地方会向 context 里塞入各种不同的 k-v 对,最后在某个地方使用。根本就不知道什么时候什么地方传了什么值?这些值会不会被“覆盖”(底层是两个不同的 context 节点,查找的时候,只会返回一个结果),context.Value 最受争议的地方,慎用

五. 问题及总结

context的缺陷及注意点

  1. context虽然实现父协程对子协程的管理,但是这种管理方式是比较粗暴的,直接关闭,而且关闭时根本不知道子协程的执行结果。总之,对子协程的管理不够细致化,必要时需要在字协程退出时用defer做下退出处理,或者你可以使用waitGroup这种,对协程的执行结果有个明确的了解
  2. 注意点:
  1. 不要把Context放在结构体中,要以参数的方式显示传递
  2. 以Context作为参数的函数方法,应该把Context作为第一个参数。
  3. 给一个函数方法传递Context的时候,不要传递nil,如果不知道传递什么,就使用context.TODO
  4. Context的Value相关方法应该传递请求域的必要数据,不应该用于传递可选参数
  5. Context是线程安全的,可以放心的在多个goroutine中传递

什么是Context

  1. context可以理解为一个并发控制工具,它与WaitGroup最大的不同点是context对于派生goroutine有更强的控制力,它可以控制多级的goroutine
  2. context是一个接口,内部分别提供了
  1. Deadline获取设置的截止时间方法,
  2. Done返回一个只读的 channel 类型是 struct{}方法,当有信号时,表明parent context 已经发起了取消,通过 Done chan 获取到取消信号后, 应当做清理操作,然后退出协程,释放资源,
  3. Err返回 ctx 为什么被取消方法,
  4. Value获取 ctx 上绑定的值
  1. golang标准库中实现context接口提供了:
  1. emptyCtx可以获取一个空context,
  2. cancelCtx: 被cancle时会把其中的所有child都cancle掉
  3. timerCtx: 在cancelCtx基础上增加了deadline用于标示自动cancel的最终时间
  1. 并且基于Context派送出了
  1. WithCancel() 生成一个带取消函数的子上下文
  2. WithDeadline() 与 WithTimeOut() 接收过期时间,生成一个带取消函数的子上下文
  1. 基于Context使用案例:
  1. 超时取消: 使用WithTimeout()获取一个超时自动取消的context, 后续在执行中通过select监听Done.执行取消业务
  2. 基于context的values缓存数据
  1. context 如何被取消:

context.Value 的查找过程

  1. 了解查找过程,首先要看一下Value的底层结构,当执行context.Value时会返回一个valueCtx 结构,该结构继承了Context,它仍然是一个 Context,并且绑定了一个String()和Value()方法
  2. 取值的过程,实际上是一个递归查找的过程, 会顺着链路一直往上找,比较当前节点的 key是否是要找的 key,如果是,则直接返回 value。否则,一直顺着 context 往前,最终找到根节点(一般是 emptyCtx)
  3. 参考博客
  1. 参考博客
  2. 参考博客
  3. 参考博客
  4. Go 语言问题集(Go Questions)

你可能感兴趣的:(#,四.,Go,高性能编程相关问题,#,五.,Go,并发安全相关,#,二.,Go,常见数据结构实现原理,golang,ios,objective-c)