When developing with Go as a server, each incoming request is assigned a Goroutine to handle, and additional Goroutines may be created to access DB or RPC services during request processing. The goroutine involved in this request may require access to specific values such as authentication token, user id, or request expiration time. When a request is cancelled or times out, the goroutine involved in the request should also be terminated so that the system can quickly reclaim the resource.
For simplicity purposes, the context package defines the context type to pass timeout, cancel signals, and Request-scope values across API boundaries and between processes. When the server makes a new request it should create a Context and the return request should accept a Context. The chain of function calls must pass the Context object, or a Context derived from WithCancel, WithDeadline, WithTime, or WithValue. When a Context is cancelled, all contexts derived from that object are cancelled.
For example, you can implement a time-out protection method using the context mechanism
func main(a) {
cancelJob(time.Second*1.func(a) error {
time.Sleep(time.Second * 10)
return nil})}func cancelJob(timeout time.Duration, f func(a) 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(a) {
e <- f()
}()
select {
case err := <-e:
return err
case <-ctx.Done():
return ctx.Err()
}
}
Copy the code
Context uses the general steps:
- 1. Build a
Context
Object, if you don’t know what Context to use, you can call it, okaycontext.Background
orcontext.TODO
- 2 According to your needs
Context
Package derivative- WithCancel :Context can be cancelled
- WithDeadline: Context You can set the deadline
- WithTimeout: Actually uses WithDeadline
- WithValue: Uses Context to pass the value
- 3 Listen for the channel ctx.Done and the channel will be closed once the Context is cancelled
- 4 At last, call the cancel method to facilitate resource reclamation
The data structure
The Context package provides two convenient ways to create context objects
- Context.Background cannot be cancelled. It has no value and no expiration date, and is usually used for main functions, initializations, tests, or as the top-level context when a new request comes in
- Context.todo is used when you don’t know what context to use
Both methods are an emptyCtx object essentially the same
var (
background = new(emptyCtx)
todo = new(emptyCtx)
)
func Background(a) Context {
return background
}
func TODO(a) Context {
return todo
}
Copy the code
In addition, the context package provides four methods: WithCancel, WithDeadline, WithTimeout, and WithValue can be derived from context to cancelCtx, timerCtx, and valueCtx, all of which implement the context. context interface
Context's methods are thread-safe
type Context interface {
// Returns when context needs to be cancelled. Ok false means deadline is not set
Deadline() (deadline time.Time, ok bool)
// Put back a closed channel when context is cancelled Done
// Done returns nil to indicate that the current context cannot be cancelled
// Done is usually used in select statements
Done() <-chan struct{}
// Returns the reason for the context cancellation
Err() error
// Returns the value associated with the key specified in context, nil if not specified
// Mainly used for request-scoped data passing between process and API boundary, not for optional parameter passing
// Key needs to support equality operations and is best defined as not being able to type everywhere to avoid confusion
Value(key interface{}) interface{}}Copy the code
These object hierarchies
Derivative contexts
Context derived with WithCancel, WithDeadline, WithTimeout, and WithValue provides functions such as cancellation, value transfer, and timeout cancellation for the original context.
WithCancel
WithCancel spawns a new cancelable Context object
// WithCancel returns the context containing a copy of the parent context and a new channel and a cancel function
// When the returned cancel function is called or the parent context's Done channcel is closed, the channel of the context returned by WitchCancel is also closed
// The cancel function should be called as soon as the operation is complete so that the resources associated with this context can be released
func WithCancel(parent Context) (ctx Context, cancel CancelFunc) {
c := newCancelCtx(parent)
// Build the relationship between parent and child contexts to ensure that the child context is cancelled when the parent context is cancelled
propagateCancel(parent, &c)
return &c, func(a) { c.cancel(true, Canceled) }
}
Copy the code
Wrap Context as cancelable Context–>cancelCtx
CancelCtx cancelCtx cancelCtx cancelCtx cancelCtx cancelCtx cancelCtx cancelCtx cancelCtx cancelCtx
type cancelCtx struct {
Context
mu sync.Mutex // Protect the following fields
done chan struct{} // Lazy creates the cancel method to close the first time it is called
children map[canceler]struct{} // Cancel is set to nil on the first call
err error // Cancel is set non-nil the first time it is called
}
Copy the code
The done channel is initialized only when cancelCtx is called. If cancelCtx subcontext can be cancelled, the Canceler interface needs to be implemented
// 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{}}Copy the code
If the parent context’s Done is nil, it cannot be cancelled. Otherwise, parentCancelCtx will be called until the cancelable parent context is found. If it is found, parentCancelCtx will be called
-
If you can find
- If the parent context has been cancelled, the child context’s cancel method is called to cancel;
- If the parent context is not cancelled, the current child context is managed by the parent context
-
If a developer custom type cannot be found, for example
Launch a Gorountine directly to listen for notifications of parent-child cancellations
// 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 {// Find the parent and cancel the context
p.mu.Lock()
ifp.err ! =nil {
// The parent context has been cancelled
child.cancel(false, p.err)
} else {// If the parent context does not cancel, the child context is given to the parent context to manage, so that the parent node can propagate the cancellation event to the child context when it cancels
if p.children == nil {
p.children = make(map[canceler]struct{})
}
p.children[child] = struct{}{}
}
p.mu.Unlock()
} else {// Cancel context if the parent is not found
go func(a) {
select {
case <-parent.Done():
child.cancel(false, parent.Err())
case <-child.Done():
}
}()
}
}
Copy the code
ParenCancelCtx loops to see if the context has a cancelable parent
// 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}}}Copy the code
Cancel the function cancel
When your business method is finished executing, you should call the Cancel method as soon as possible to reclaim resources quickly
If removeFromParent is true, c is removed from the parent's child context
func (c *cancelCtx) cancel(removeFromParent bool, err error) {
if err == nil {
panic("context: internal error: missing cancel error")
}
c.mu.Lock()
ifc.err ! =nil {
c.mu.Unlock()
return // if c.elr is not nil, the current context has been cancelled
}
c.err = err
if c.done == nil { // Calling the cancel method is initialized at this point
c.done = closedchan//closedchan is the closedchan as its name indicates
} else {/ / close the c.d one
close(c.done)
}
// Cancel the subcontext
for child := range c.children {
// The parent context lock is held and the child context lock is acquired
child.cancel(false, err)
}
c.children = nil // Cancel completes set nil
c.mu.Unlock()
if removeFromParent {// Remove the current context from its parent's child context collection
removeChild(c.Context, c)
}
}
Copy the code
WithDeadline
Make context timeout cancel
// WithDeadline returns a context containing a copy of the parent context and a context WithDeadline d, if the parent deadline is earlier than d
WithDeadline(parent, d) is semantically equivalent to the parent context
The done channel in the context returned by WithDeadline is also closed when the deadline expires or the returned cancel function is called or the parent context's Done channel is closed
// The cancel function should be called as soon as the operation is complete so that the resources associated with this context can be released
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) // The deadline has expired
return c, func(a) { c.cancel(false, Canceled) }
}
c.mu.Lock()
defer c.mu.Unlock()
if c.err == nil {
// Time monitor
c.timer = time.AfterFunc(dur, func(a) {
c.cancel(true, DeadlineExceeded)
})
}
return c, func(a) { c.cancel(true, Canceled) }
}
Copy the code
TimerCtx is embedded with cancelCtx, and the main cancel capability is brokered. In addition, an expiration time and a timer are added. If the expiration time is not reached and the context is not cancelled, a timer will be started. If the context expires, cancel will be performed
// timerCtx contains a timer and a cancelCtx embedded within it to implement the Done and Err methods
// Cancel by stopping the timer and then calling cancelctx.cancel
type timerCtx struct {
cancelCtx
timer *time.Timer // Under cancelCtx.mu.
deadline time.Time
}
Copy the code
The Cancel operation of timerCtx itself stops the timer, and then the main Cancel operation agent is given to 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()
ifc.timer ! =nil {
c.timer.Stop()
c.timer = nil
}
c.mu.Unlock()
}
Copy the code
WithTimeut
The actual use of WithDeadline is nothing to say.
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc) {
return WithDeadline(parent, time.Now().Add(timeout))
}
Copy the code
So basically, the cancelable context holds the collection of children through the parent and if the parent cancels, then the context in the collection of children is called cancel in turn.
WithValue
Add a Value(key interface{}) interface{} method to get the Value associated with the Context based on the specified key. The logic is simple and there’s nothing to talk about.
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{}}Copy the code
conclusion
The core implementation of the Context package is only 200-300 lines, excluding comments. The overall implementation is still short and concise, providing data transfer across process and API boundaries, as well as concurrency and timeout cancellation. The practical application process also brings great convenience to our technical implementation, such as the implementation of full link trace. It is recommended that we use the context as the first parameter of the function, but it is still a mental burden for many people to use it. Therefore, some people make a Goroutine local storage to avoid writing the context