[TOC]

Golang Context in depth

Context Context and application scenarios

Golang did not have its own context in 1.6.2, but the golang.org/x/net/context package was added to the official library in 1.7. Golang’s Context package is designed to simplify operations related to data, cancellation signals, deadlines, etc. in the request domain between multiple Goroutines handling a single request, which may involve multiple API calls.

For example, if you have a network Request Request, each Request needs to open a Goroutine to do something, and those Goroutines may open other Goroutines. So, we can track these Goroutines by Context, and we can control their purpose by Context, and that’s what the Go language provides us with, which is called “Context” in Chinese.

As another practical example, in the Go server program, each request is processed by a Goroutine. However, handlers often need to create additional Goroutines to access back-end resources, such as databases, RPC services, and so on. Since these Goroutines are all processing the same request, they often need access to shared resources, such as user identity information, authentication token, request deadline, and so on. And if the request times out or is cancelled, all goroutines should immediately exit and release the associated resources. In this case we also need to use the Context to cancel all the goroutines for us

You can obtain the package by using the go get golang.org/x/net/context command.

The Context definition

The main data structure of ontext is a nested structure or one-way inheritance structure. For example, the original context is a small box containing some data, and the children inherited from this context are like a box inside the original context. And then it contains some of its own data. In other words, context is a hierarchical structure. According to different scenarios, each layer of context has some different characteristics. This hierarchical organization also makes context easy to expand, and its responsibilities are clear.

Struct context; struct context; struct context;

type Context interface {

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

Done() <-chan struct{}

Err() error

Value(key interface{}) interface{}

}
Copy the code

You can see that Context is an interface, and in Golang, interface is a very widely used structure that can accept any type. The Context definition is very simple, there are four methods, and we need to be able to understand these methods very well

  1. The Deadline method gets the set Deadline. The first callback is the Deadline, at which point the Context will automatically initiate a cancellation request. The second return value, ok==false, indicates that no cutoff time is set, and that cancelling is required.

  2. The Done method returns a read-only chan of type struct{}. In goroutine, if the chan can be read from the parent context, it means that the parent context has issued a cancellation request. Then exit Goroutine, freeing resources. After that, the Err method returns an error telling you why the Context was canceled.

  3. The Err method returns the cause of the cancellation error because the Context was canceled.

  4. The Value method gets the Value bound to the Context, which is a key-value pair, so you need a Key to get the corresponding Value, which is generally thread-safe.

How to implement Context

Context is an interface, but you don’t need to use the method implementation. Golang’s built-in Context package already implements two methods. In code, we usually start a Context with these two as the top parent Context, and then derive the child Context. These Context objects form a tree: when a Context object is cancelled, all contexts inherited from it are cancelled. The two implementations are as follows:

var (
	background = new(emptyCtx)

	todo = new(emptyCtx)
)

func Background() Context {

	return background

}

func TODO() Context {

	return todo
}
Copy the code

One is Background, which is used in the main function, initialization, and test code. As the Context, the top Context in the tree, the root Context, cannot be cancelled.

One is TODO, which we can use if we don’t know what Context to use, but we haven’t used this TODO yet.

Both of them are essentially emptyCtx struct types, which are non-cancelable, have no expiration date, and carry no values.

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
}
Copy the code

The Context of inheritance

So with the root Context above, how do you derive more child contexts? This depends on the With series of functions provided by the context package.

func WithCancel(parent Context) (ctx Context, cancel CancelFunc)

func WithDeadline(parent Context, deadline time.Time) (Context, CancelFunc)

func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc)

func WithValue(parent Context, key, val interface{}) Context

Copy the code

Using these functions, you create a Context tree, where each node can have as many children as you want, and the node hierarchy can be as many.

The WithCancel function, which passes a parent Context as an argument, returns a child Context, and a cancel function to cancel the Context.

The WithDeadline function, which is similar to WithCancel, it’s going to pass in an extra deadline, which means that at this point in time, the Context is going to cancel automatically, but we can also wait until that point, we can cancel ahead of time with the cancel function.

So WithTimeout is basically the same thing as WithDeadline, so this means automatically cancels out, how much time after the Context is automatically cancelled.

The WithValue function has nothing to do with canceling the Context. It generates a Context that is bound to a key-value pair of data, which can be accessed through the context. Value method. This is a trick we often use in practice. This can be used, for example, when we need tarce to trace the system call stack.

With series of functions in detail

WithCancel

Context. WithCancel generates an instance of WithCancel and a cancelFuc, which is used to close the Done Channel function in ctxWithCancel.

Let’s analyze the source code implementation, first look at initialization, as follows:

func newCancelCtx(parent Context) cancelCtx {
	return cancelCtx{
		Context: parent,
		done:    make(chan struct{}),
	}
}

func WithCancel(parent Context) (ctx Context, cancel CancelFunc) {
	c := newCancelCtx(parent)
	propagateCancel(parent, &c)
	return &c, func() { c.cancel(true, Canceled) }
}
Copy the code

NewCancelCtx returns an initialized cancelCtx. The cancelCtx constructor inherits the Context and implements the canceler method:

CancelCtx and timerCtx both implement canceler. The type that implements the canceler interface can be canceled directlytype canceler interface {
    cancel(removeFromParent bool, err error)
    Done() <-chan struct{}
}


type cancelCtx struct {
    Context
    done chan struct{} // closed by the first cancel call.
    mu       sync.Mutex
    children map[canceler]struct{} // setTo nil by the first cancel call err error // Err will be set to non-nil} func (c *cancelCtx) Done() <-chan struct{} {return c.done
}

func (c *cancelCtx) Err() error {
    c.mu.Lock()
    defer c.mu.Unlock()
    return c.err
}

func (c *cancelCtx) String() string {
    return fmt.Sprintf("%v.WithCancel", c. context)} // the core is closed c. one // and sets c.err = err, c. dren = nil. Cancel each child separately // If removeFromParent is set, CancelCtx cancel(removeFromParent bool, err error) {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 // already canceled
    }
    c.err = err
    close(c.done)
    for child := range c.children {
        // NOTE: acquiring the child's lock while holding parent's lock.
        child.cancel(false, err)
    }
    c.children = nil
    c.mu.Unlock()

    ifCancelCtx {removeFromParent {removeChild(c.text, c)}}Copy the code

As you can see, all the children are in one map; The Done method returns the Done channel, while the other cancel method closes the Done Channel and iterates layer by layer, closing the Children’s channel and removing the current Canceler from parent.

WithCancel initializes a cancelCtx, executes the propagateCancel method, and finally returns a Cancel function.

The propagateCancel method is defined as follows:

// 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 {
		p.mu.Lock()
		ifp.err ! = nil { // parent has already been canceled child.cancel(false, p.err)
		} else {
			if p.children == nil {
				p.children = make(map[canceler]struct{})
			}
			p.children[child] = struct{}{}
		}
		p.mu.Unlock()
	} else {
		go func() {
			select {
			case <-parent.Done():
				child.cancel(false, parent.Err())
			case <-child.Done():
			}
		}()
	}
}
Copy the code

* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * Cancel the child tree; otherwise, connect the child node directly to the children of the parent. If the nearest parent that can be cancelled is not found, then a Goroutine is launched to wait for the parent to terminate, cancel the child tree passed in, or wait for the child to terminate.

WithDeadLine

If the time is up, cancel will be performed. The specific operation process is basically the same as withCancel, except that the timing of cancel function call is controlled by a timeout channel.

Context uses principles and techniques

  • Don’t put the Context in a structure, pass it as a parameter. Parent Context is usually Background
  • Context should be passed as the first argument to every function on the inbound request and outbound request link, with the variable name recommended as CTX.
  • When you pass Context to a function method, don’t pass nil, otherwise you’ll break the connection when tarce traces
  • The Value related methods of the Context should pass the necessary data, don’t use this pass for everything
  • Context is thread-safe and can be passed safely across multiple Goroutines
  • You can pass a Context object to any number of Gorotuines, and when you cancel it, all goroutines will receive the cancel signal.

Common method instances of Context

  1. Call the Context Done method to cancel

    func Stream(ctx context.Context, out chan<- Value) error {
    
    	for {
    		v, err := DoSomething(ctx)
    
    		iferr ! = nil {return err
    		}
    		select {
    		case <-ctx.Done():
    
    			return ctx.Err()
    		case out <- v:
    		}
    	}
    }
    
    Copy the code
  2. Pass the value through context.withValue

    func main() {
    	ctx, cancel := context.WithCancel(context.Background())
    
    	valueCtx := context.WithValue(ctx, key, "add value")
    
    	go watch(valueCtx)
    	time.Sleep(10 * time.Second)
    	cancel()
    
    	time.Sleep(5 * time.Second)
    }
    
    func watch(ctx context.Context) {
    	for {
    		select {
    		case <-ctx.Done():
    			//get value
    			fmt.Println(ctx.Value(key), "is cancel")
    
    			return
    		default:
    			//get value
    			fmt.Println(ctx.Value(key), "int goroutine")
    
    			time.Sleep(2 * time.Second)
    		}
    	}
    }
    
    Copy the code
  3. Cancels context.withTimeout

    package main
    
    import (
    	"fmt"
    	"sync"
    	"time"
    
    	"golang.org/x/net/context"
    )
    
    var (
    	wg sync.WaitGroup
    )
    
    func work(ctx context.Context) error {
    	defer wg.Done()
    
    	for i := 0; i < 1000; i++ {
    		select {
    		case <-time.After(2 * time.Second):
    			fmt.Println("Doing some work ", i)
    
    		// we received the signal of cancelation in this channel
    		case <-ctx.Done():
    			fmt.Println("Cancel the context ", i)
    			return ctx.Err()
    		}
    	}
    	return nil
    }
    
    func main() {
    	ctx, cancel := context.WithTimeout(context.Background(), 4*time.Second)
    	defer cancel()
    
    	fmt.Println("Hey, I'm going to do some work")
    
    	wg.Add(1)
    	go work(ctx)
    	wg.Wait()
    
    	fmt.Println("Finished. I'm going home")}Copy the code
  4. Cancels context.withdeadline

    package main
    
    import (
    	"context"
    	"fmt"
    	"time"
    )
    
    func main() {
    	d := time.Now().Add(1 * time.Second)
    	ctx, cancel := context.WithDeadline(context.Background(), d)
    
    	// Even though ctx will be expired, it is good practice to call its
    	// cancelation function in any case. Failure to do so may keep the
    	// context and its parent alive longer than necessary.
    	defer cancel()
    
    	select {
    	case <-time.After(2 * time.Second):
    		fmt.Println("oversleep")
    	case <-ctx.Done():
    		fmt.Println(ctx.Err())
    	}
    }
    
    Copy the code

reference

Snow ruthless blog

Welcome to follow my wechat official account: Linux server system development, and we will vigorously send quality articles through wechat official account in the future.