使用Go
作为服务端开发时,每个请求过来都会分配一个goroutine
来处理,请求处理过程中,可能还会创建额外的goroutine
访问DB或者RPC服务。这个请求涉及的goroutine
可能需要访问一些特定的值比如认证token、用户标识或者请求截止时间。当一个请求取消或者超时,则这个请求涉及的goroutine
也应该被终止,这样系统就可以快速回收这部分资源。
基于简化目的,context
包定义了Context
类型,来传递超时、取消信号以及跨API边界和进程之间request-scope
值。当服务器来新的请求应该创建一个Context
并且返回请求应该接受一个Context
。这其中的函数调用链必须传递Context
对象,或者是通过WithCancel
、WithDeadline
、WithTime
或WithValue
衍生的Context
对象。当一个Context
取消,所有从这个对象衍生的Context
都会被取消。
例如你可以利用context
的这种机制,实现一个任务超时保护的方法
func main() {
cancelJob(time.Second*1, func() error {
time.Sleep(time.Second * 10)
return nil
})
}
func cancelJob(timeout time.Duration, f func() error) error {
var (
ctx context.Context
cancelFunc context.CancelFunc
)
if timeout > 0 {
ctx, cancelFunc = context.WithTimeout(context.Background(), timeout)
} else {
ctx, cancelFunc = context.WithCancel(context.Background())
}
defer cancelFunc()
e := make(chan error, 1)
go func() {
e <- f()
}()
select {
case err := <-e:
return err
case <-ctx.Done():
return ctx.Err()
}
}
Context
使用大致步骤:
- 1 构建一个
Context
对象,如果你不知道该使用什么Context合适,可以调用context.Background
或context.TODO
- 2 根据你的需求可以对
Context
进行包装衍生- WithCancel :Context可取消
- WithDeadline: Context可设置截止时间
- WithTimeout:实际使用的是WithDeadline
- WithValue: 需要使用Context传值
- 3 监听ctx.Done这个channel 一旦Context取消 此channel会被关闭
- 4 最后在方法处理完毕时请及时调用cancel方法 方便资源回收
数据结构
context包提供了两种创建Context对象的便捷方式
- context.Background 无法被取消 没有值 没有截止时间,通常用于主函数、初始化、测试或者当新请求来了作为顶层Context
- context.TODO 当你不知道用啥Context的时候使用
这两种方式都是一个emptyCtx
对象 本质上没啥差别
var (
background = new(emptyCtx)
todo = new(emptyCtx)
)
func Background() Context {
return background
}
func TODO() Context {
return todo
}
此外context包提供了4个方法:WithCancel
、WithDeadline
、WithTimeout
、WithValue
,可以对context进行衍生为cancelCtx
、timerCtx
、valueCtx
,他们都实现了context.Context
接口
// Context的方法是线程安全的
type Context interface {
// 返回context何时需要被取消 ok为false表示deadline未设置
Deadline() (deadline time.Time, ok bool)
// 当context被取消 Done放回一个关闭的channel
// Done返回nil 表示当前context不能被取消
// Done通常在select语句中使用
Done() <-chan struct{}
// 返回context取消的原因
Err() error
// 返回context中指定的key关联的value,未指定返回nil
// 主要用作在进程和API边界间传递request-scoped数据,不要用于可选参数传递
// key需要支持相等操作,最好定义为不可到处类型 避免混淆
Value(key interface{}) interface{}
}
这几个对象层次结构
衍生contexts
通过WithCancel
、WithDeadline
、WithTimeout
、WithValue
方法衍生的context为原始context提供了取消、传值、超时取消等功能。
WithCancel
通过WithCancel
衍生出新的可取消的Context
对象
// WithCancel 返回包含父context拷贝和一个新的channel的context 和一个cancel函数
// 当返回的cancel函数被调用或父context的done channcel关闭 则WitchCancel返回的context的channel也会被关闭
// 当操作完成时应该尽快调用cancel函数 这样就可以释放此context关联的资源
func WithCancel(parent Context) (ctx Context, cancel CancelFunc) {
c := newCancelCtx(parent)
// 构建父子上下文之间的关系 保证父上下文取消时子上下文也会被取消
propagateCancel(parent, &c)
return &c, func() { c.cancel(true, Canceled) }
}
将Context包装为可取消的Context
-->cancelCtx
// cancelCtx可以被取消,当取消时,也会将其实现了canceler的子context也取消
type cancelCtx struct {
Context
mu sync.Mutex // 保护下面的字段
done chan struct{} // 惰性创建 cancel方法第一次调用时关闭
children map[canceler]struct{} // cancel第一次调用时置为nil
err error // cancel第一次调用时设置non-nil
}
其中done
这个channel是在cancel调用的时候才会被初始化,cancelCtx
子context若可取消需要实现canceler
接口
// A canceler is a context type that can be canceled directly. The
// implementations are *cancelCtx and *timerCtx.
type canceler interface {
cancel(removeFromParent bool, err error)
Done() <-chan struct{}
}
构建可取消context后,后将取消操作进行传播,如果父context的Done为nil,表示其不可取消直接返回,否则会调用parentCancelCtx
直到找到可取消父context ,若找到
-
若可以找到则
- 且父context已经取消则会调用子context的cancel方法进行取消;
- 且父context未取消则将当前子context交给父context管理
-
若找不到 例如开发者自定义的类型则
直接启动一个
gorountine
来监听父子取消事件通知
// propagateCancel arranges for child to be canceled when parent is.
func propagateCancel(parent Context, child canceler) {
if parent.Done() == nil {
return // parent is never canceled
}
if p, ok := parentCancelCtx(parent); ok {//找到父可取消context
p.mu.Lock()
if p.err != nil {
// 父context已经被取消 取消子context
child.cancel(false, p.err)
} else {// 父context未cancel 则将子context交给父context管理,方便父节点取消时将取消事件传播给子context
if p.children == nil {
p.children = make(map[canceler]struct{})
}
p.children[child] = struct{}{}
}
p.mu.Unlock()
} else {//找不到父可取消context
go func() {
select {
case <-parent.Done():
child.cancel(false, parent.Err())
case <-child.Done():
}
}()
}
}
parenCancelCtx
循环查找context是否存在可取消父节点
// parentCancelCtx follows a chain of parent references until it finds a
// *cancelCtx. This function understands how each of the concrete types in this
// package represents its parent.
func parentCancelCtx(parent Context) (*cancelCtx, bool) {
for {
switch c := parent.(type) {
case *cancelCtx:
return c, true
case *timerCtx:
return &c.cancelCtx, true
case *valueCtx:
parent = c.Context
default:
return nil, false
}
}
}
取消函数cancel
当你的业务方法执行完毕,你应该尽快调用cancel方法,这样方便快速回收相关资源
//关闭c.done,取消掉c的子context,若removeFromParent为true,则将c从父context的子context集合中删除
func (c *cancelCtx) cancel(removeFromParent bool, err error) {
if err == nil {
panic("context: internal error: missing cancel error")
}
c.mu.Lock()
if c.err != nil {
c.mu.Unlock()
return // c.err不为nil 则表示当前context已经被取消
}
c.err = err
if c.done == nil { //调用cancel方法此时才初始化
c.done = closedchan//closedchan如其名字 为已经关闭的chan
} else {//关闭c.done
close(c.done)
}
//对子context进行cancel
for child := range c.children {
// 此处在持有父context锁的同时 获取子context的锁
child.cancel(false, err)
}
c.children = nil //cancel完毕 置nil
c.mu.Unlock()
if removeFromParent {//将当前context从其父context的子context集合中删除
removeChild(c.Context, c)
}
}
WithDeadline
让context具备超时取消功能
// WithDeadline 返回包含父context拷贝和deadline为d的context,如果父deadline早于d
// 则语义上WithDeadline(parent, d) 和父context是相等的
//当deadline过期了 或者返回的cancel函数被调用 或者父context的done channel关闭了则WithDeadline返回的context中的done channel也会关闭
// 当操作完成时应该尽快调用cancel函数 这样就可以释放此context关联的资源
func WithDeadline(parent Context, d time.Time) (Context, CancelFunc) {
if cur, ok := parent.Deadline(); ok && cur.Before(d) {
// The current deadline is already sooner than the new one.
return WithCancel(parent)
}
c := &timerCtx{
cancelCtx: newCancelCtx(parent),
deadline: d,
}
propagateCancel(parent, c)
dur := time.Until(d)
if dur <= 0 {
c.cancel(true, DeadlineExceeded) // deadline已经到期
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) }
}
timerCtx
内嵌了cancelCtx
,主要的cancel能力进行了代理。额外新增了一个截止时间和一个定时器,初始化此类context时如果未到截止时间且未取消 则会启动一个定时器,超时即会执行cancel操作
// timerCtx包含了一个定时器和一个截止时间 内嵌一个cancelCtx来实现 Done和Err方法
// 取消操作通过停止定时器然后调用cancelCtx.cancel来实现
type timerCtx struct {
cancelCtx
timer *time.Timer // Under cancelCtx.mu.
deadline time.Time
}
timerCtx
的cancel操作本身会停掉定时器,然后主要cancel操作代理给了cancelCtx
func (c *timerCtx) cancel(removeFromParent bool, err error) {
c.cancelCtx.cancel(false, err)
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()
}
WithTimeut
实际掉用了WithDeadline
没啥好说的。
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc) {
return WithDeadline(parent, time.Now().Add(timeout))
}
总的来说,就是可取消context
通过父节点保存子节点集合 若父节点取消则将子节点集合中的context依次调用cancel方法。
WithValue
赋予了Context传值能力,Context的能力代理给了父context,自身新增了一个Value(key interface{}) interface{}
方法,根据指定key获取跟context关联的value,逻辑比较简单 没啥好说的。
func WithValue(parent Context, key, val interface{}) Context {
if key == nil {
panic("nil key")
}
if !reflectlite.TypeOf(key).Comparable() {
panic("key is not comparable")
}
return &valueCtx{parent, key, val}
}
// A valueCtx carries a key-value pair. It implements Value for that key and
// delegates all other calls to the embedded Context.
type valueCtx struct {
Context
key, val interface{}
}
总结
context
包核心实现除去注释只有200-300行,整体实现还是短小精悍,为我们提供了跨进程、API边界的数据传递以及并发、超时取消等功能。实际应用过程中也给我们技术实现带来很大便利,比如全链路trace的实现。官方建议我们将context作为函数第一个参数使用,不过实际使用过程中还是会给不少人代理心智负担,所以有人为了尽可能不写context,搞了个Goroutine local storage 有兴趣可以研究下