我们先来看一下context.Context
的接口:
// A Context carries a deadline, a cancellation signal, and other values across
// API boundaries.
//
// Context's methods may be called by multiple goroutines simultaneously.
type Context interface {
// Deadline returns the time when work done on behalf of this context
// should be canceled. Deadline returns ok==false when no deadline is
// set. Successive calls to Deadline return the same results.
Deadline() (deadline time.Time, ok bool)
// Done returns a channel that's closed when work done on behalf of this
// context should be canceled. Done may return nil if this context can
// never be canceled. Successive calls to Done return the same value.
// The close of the Done channel may happen asynchronously,
// after the cancel function returns.
//
// WithCancel arranges for Done to be closed when cancel is called;
// WithDeadline arranges for Done to be closed when the deadline
// expires; WithTimeout arranges for Done to be closed when the timeout
// elapses.
//
// Done is provided for use in select statements:
//
// // Stream generates values with DoSomething and sends them to out
// // until DoSomething returns an error or ctx.Done is closed.
// 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:
// }
// }
// }
//
// See https://blog.golang.org/pipelines for more examples of how to use
// a Done channel for cancellation.
Done() <-chan struct{}
// If Done is not yet closed, Err returns nil.
// If Done is closed, Err returns a non-nil error explaining why:
// Canceled if the context was canceled
// or DeadlineExceeded if the context's deadline passed.
// After Err returns a non-nil error, successive calls to Err return the same error.
Err() error
// Value returns the value associated with this context for key, or nil
// if no value is associated with key. Successive calls to Value with
// the same key returns the same result.
//
// Use context values only for request-scoped data that transits
// processes and API boundaries, not for passing optional parameters to
// functions.
//
// A key identifies a specific value in a Context. Functions that wish
// to store values in Context typically allocate a key in a global
// variable then use that key as the argument to context.WithValue and
// Context.Value. A key can be any type that supports equality;
// packages should define keys as an unexported type to avoid
// collisions.
//
// Packages that define a Context key should provide type-safe accessors
// for the values stored using that key:
//
// // Package user defines a User type that's stored in Contexts.
// package user
//
// import "context"
//
// // User is the type of value stored in the Contexts.
// type User struct {...}
//
// // key is an unexported type for keys defined in this package.
// // This prevents collisions with keys defined in other packages.
// type key int
//
// // userKey is the key for user.User values in Contexts. It is
// // unexported; clients use user.NewContext and user.FromContext
// // instead of using this key directly.
// var userKey key
//
// // NewContext returns a new Context that carries value u.
// func NewContext(ctx context.Context, u *User) context.Context {
// return context.WithValue(ctx, userKey, u)
// }
//
// // FromContext returns the User value stored in ctx, if any.
// func FromContext(ctx context.Context) (*User, bool) {
// u, ok := ctx.Value(userKey).(*User)
// return u, ok
// }
Value(key any) any
}
我们来逐一解释一下这些方法的含义:
Deadline
– 返回context.Context
被取消的时间,即完成工作的截止日期Done
– 返回一个Channel
,这个Channel
会在当前工作完成或上下文被取消之后关闭,多次调用该方法会返回同一个Channel
Err
返回Context
结束的原因Value
从context.Context
中获取键对应的值,对于同一个上下文来说,多次调用Value
并传入相同的key
会返回相同的结果,该方法可以用来传递特定的数据
设计原理
context.Context
的最大作用是,在Goroutine
构成的树形结构中同步信号以减少计算资源的浪费。每一个context.Context
都会从最顶层的Goroutine
逐层传递到最底层。
下面我们用一个事例来看一下:
在讲解事例之前我们先来认识一下context.WithTimeout
函数:
// WithTimeout returns WithDeadline(parent, time.Now().Add(timeout)).
//
// Canceling this context releases resources associated with it, so code should
// call cancel as soon as the operations running in this Context complete:
//
// func slowOperationWithTimeout(ctx context.Context) (Result, error) {
// ctx, cancel := context.WithTimeout(ctx, 100*time.Millisecond)
// defer cancel() // releases resources if slowOperation completes before timeout elapses
// return slowOperation(ctx)
// }
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc) {
return WithDeadline(parent, time.Now().Add(timeout))
}
我们创建一个过期时间为1s的上下文,并向上下文传入handle
函数,它处理请求的时间是500ms
func main() {
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
// 为什么要写这一句话?原因很简单,如果上下文到达了它之前规定的超时时间,它会发送Done信号,然后消耗释放上下文资源,但是如果在超时之前,程序就执行完毕了,那么就会造成内存泄漏,因此要加上这句话,确保可以释放上下文的内存
defer cancel()
go handle(ctx, 500*time.Millisecond)
select {
case <-ctx.Done():
fmt.Println("main", ctx.Err())
}
}
func handle(ctx context.Context, duration time.Duration) {
select {
case <-ctx.Done():
fmt.Println("handle", ctx.Err())
case <-time.After(duration):
fmt.Println("process request with", duration)
}
}
因为过期时间大于处理时间,所以我们有足够时间处理请求,将打印以下信息。
process request with 500ms
main context deadline exceeded
如果把处理请求的时间换成1500ms,那么整个程序会因为上下文过期而终止。
取消信号
context.WithCancel
函数能够从context.Context
中衍生出新的子上下文,并返回用于取消该上下文的函数。一旦我们执行返回的取消函数,当前上下文及其子上下文都会被取消,所有Goroutine
都会同步收到这一取消信号。
// WithCancel returns a copy of parent with a new Done channel. The returned
// context's Done channel is closed when the returned cancel function is called
// or when the parent context's Done channel is closed, whichever happens first.
//
// Canceling this context releases resources associated with it, so code should
// call cancel as soon as the operations running in this Context complete.
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) }
}
context.newCancelCtx
将传入的上下文包装成私有结构体context.cancelCtx
;context.propagateCancel
会构建父子上下文之间的关联,当父上下文被取消时,子上下文也会被取消:
传值方法
在最后我们需要了解如何使用上下文传值,context
包中的 context.WithValue
能从父上下文中创建一个子上下文,传值的子上下文使用 context.valueCtx
类型:
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}
}
context.valueCtx
结构体会将除了 Value
之外的 Err
、Deadline
等方法代理到父上下文中,它只会响应 context.valueCtx.Value
方法,该方法的实现也很简单:
type valueCtx struct {
Context
key, val interface{}
}
func (c *valueCtx) Value(key interface{}) interface{} {
if c.key == key {
return c.val
}
return c.Context.Value(key)
}
如果 context.valueCtx
中存储的键值对与 context.valueCtx.Value
方法中传入的参数不匹配,就会从父上下文中查找该键对应的值直到某个父上下文中返回 nil
或者查找到对应的值。