Standard library context in golang

In the Go http package Server, each request has a corresponding goroutine to process. Request handlers usually start additional goroutines to access back-end services, such as databases and RPC services. The goroutine used to process a request usually needs to access some request specific data, such as end-user authentication information, authentication related token s, and the deadline of the request. When a request is cancelled or timed out, all goroutines used to process the request should exit quickly, and then the system can release the resources occupied by these goroutines.

Why do I need Context

  • Basic example
var wg sync.WaitGroup

func main() {
	wg.Add(1)
	go worker()
	wg.Wait()
}

func worker() {
	for {
		fmt.Println("worker Yes")
		time.Sleep(time.Second)
	}
	wg.Done()
}

Global variable mode

var wg sync.WaitGroup
var exit bool

// Problems with global variable mode:
// 1. It is not easy to unify the use of global variables in cross package calls
// 2. If goroutine is started again in the worker, it is not easy to control

func main() {
	wg.Add(1)
	go worker()
	time.Sleep(time.Second * 3)  // Wait 3 seconds to prevent the program from exiting too quickly
	exit = true  // Modify the global variable to exit the child goroutine
	wg.Wait()  // The main go process waits for the sub go process to end
}

func worker() {
	for {
		fmt.Println("worker Yes")
		time.Sleep(time.Second)
		if exit {
			break
		}
	}
	wg.Done()
}

Channel mode

var wg sync.WaitGroup
var ch = make(chan string)

// Problems in pipeline mode:
// 1. It is not easy to achieve standardization and unification when using global variables in cross package calls, and a common channel needs to be maintained

func main() {
	wg.Add(1)
	go worker()
	time.Sleep(time.Second * 3) // Wait 3 seconds to prevent the program from exiting too quickly
	ch <- "exit"
	close(ch)
	wg.Wait() // The main go process waits for the sub go process to end
}

func worker() {
loop:
	for {
		fmt.Println("worker Yes")
		time.Sleep(time.Second)
		select {
		case <-ch:
			break loop
		default:

		}
	}
	wg.Done()
}

Official version of the scheme

var wg sync.WaitGroup

func main() {
	ctx, cancel := context.WithCancel(context.Background())
	wg.Add(1)
	go worker(ctx)
	time.Sleep(time.Second * 3) // Wait 3 seconds to prevent the program from exiting too quickly
	cancel()  // Notify goroutine to end
	wg.Wait() 
}

func worker(ctx context.Context) {
	loop:
	for {
		fmt.Println("worker Yes")
		time.Sleep(time.Second)
		select {
		case <-ctx.Done():  // Waiting for superior notice
			break loop
		default:
		}
	}
	wg.Done()
}

When a child goroutine opens another goroutine, you only need to pass ctx in:

var wg sync.WaitGroup

func main() {
	ctx, cancel := context.WithCancel(context.Background())
	wg.Add(1)
	go worker(ctx)
	time.Sleep(time.Second * 3) // Wait 3 seconds to prevent the program from exiting too quickly
	cancel()                    // Notify goroutine to end
	wg.Wait()
}

func worker(ctx context.Context) {
	go worker2(ctx)
loop:
	for {
		fmt.Println("worker Yes")
		time.Sleep(time.Second)
		select {
		case <-ctx.Done(): // Waiting for superior notice
			break loop
		default:
		}
	}
	wg.Done()
}
func worker2(ctx context.Context) {
loop:
	for {
		fmt.Println("workder2 gagaga")
		time.Sleep(time.Second * 1)
		select {
		case <-ctx.Done():  // Waiting for superior notice
			break loop
		default:
		}
	}
}

context first acquaintance

Go1.7 adds a new standard library context, which defines the context type, which is specially used to simplify the data, cancellation signal, deadline and other related operations between multiple goroutine s processing a single request and the request domain. These operations may involve multiple API calls.
Incoming requests to the server should create context, while outgoing calls to the server should accept context. The function call chain between them must pass a context or a derived context that can be created using WithCancel, WithDeadline, WithTimeout, or WithValue. When a context is cancelled, all contexts derived from it are also cancelled.

Context interface

context.Context is an interface that defines four methods to be implemented. The signature is as follows:

type Context interface {
    Deadline() (deadline time.Time, ok bool)
    Done() <-chan struct{}
    Err() error
    Value(key interface{}) interface{}
}

Of which:

The deadline method needs to return the time when the current Context is cancelled, that is, the deadline for completing the work (deadline);
The Done method needs to return a Channel, which will be closed after the current work is completed or the context is cancelled. Calling the Done method multiple times will return the same Channel;
The Err method will return the reason why the current Context ends. It will only return a non empty value when the Channel returned by Done is closed;
If the current Context is cancelled, a cancelled error will be returned;
If the current Context times out, the DeadlineExceeded error will be returned;
The Value method will return the Value corresponding to the Key from the Context. For the same Context, calling Value multiple times and passing in the same Key will return the same result. This method is only used to transfer data across API s and between processes and request domains;

Background() and TODO()

Go has two built-in functions: Background() and TODO(). These two functions return a background and todo that implement the Context interface respectively. At the beginning of our code, these two built-in Context objects are used as the top-level part Context to derive more sub Context objects.

Background() is mainly used in the main function, initialization and test code as the top-level Context of the Context tree structure, that is, the root Context.

TODO(), which doesn't know the specific usage scenario at present, can be used if we don't know what Context to use.

background and todo are essentially emptyCtx structure types. They are a Context that cannot be cancelled, has no deadline set, and carries no value.

With series functions

In addition, four With series functions are defined in the context package.

WithCancel

The function signature of WithCancel is as follows:
func WithCancel(parent Context) (ctx Context, cancel CancelFunc)
WithCancel returns a copy of the parent node with the new Done channel. When the cancel function returned is called or when the Done channel of the parent context is closed, the Done channel of the returned context is closed, no matter what happens first.

Canceling this context frees the resources associated with it, so the code should call cancel immediately after the operation running in this context is completed.

func main() {
	ctx, cancel := context.WithCancel(context.Background())
	defer cancel()  // When we get the required integer, call cancel.
	for n := range gen(ctx) {
		fmt.Println(n)
		if n == 5 {
			break
		}
	}
}

func gen(ctx context.Context) <-chan int {  // Return to read only channel
	dst := make(chan int)
	n := 1
	go func() {
		for {
			select {
			case <-ctx.Done():
				return  // End the goroutine to prevent leakage
			case dst <- n:  // Write the n number to the channel
				n++
			}
		}
	}()
	return dst
}

In the example code above, the gen function generates integers in a separate goroutine and sends them to the returned channel. The caller of Gen needs to cancel the context after using the generated integer to avoid leakage of the internal goroutine started by Gen.

WithDeadline

WithDeadline The signature of the function is as follows:

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

Returns a copy of the parent context and adjusts the deadline to be no later than D. If the deadline of the parent context is earlier than D, then WithDeadline(parent, d) is semantically equivalent to the parent context. When the deadline expires, when the returned cancel function is called, or when the Done channel of the parent context is closed, the Done channel of the returned context will be closed, whichever occurs first.
Canceling this context frees the resources associated with it, so the code should call cancel immediately after the operation running in this context is completed.

func main() {
	d := time.Now().Add(50 * time.Millisecond)
	ctx, cancel := context.WithDeadline(context.Background(), d)

	// Although ctx will expire, it is good practice to call its cancel function in any case.
	// If you don't, you may keep the context and its parent classes alive longer than necessary.
	defer cancel()

	select {
	case <-time.After(time.Second):
		fmt.Println("overSleep")
	case <-ctx.Done():
		fmt.Println(ctx.Err())
	}
}

In the above code, we define a deadline that expires after 50 milliseconds, and then we call context.WithDeadline(context.Background(), d) to get a context (ctx) and a cancel function (cancel), and then use a select to put the main program into waiting: wait for 1 second to print overslept exit or wait for ctx to expire. Because ctx expires in 50 seconds, ctx.Done() will receive the value first, and the above code will print the reason for ctx.Err() cancellation.

WithTimeout

The function signature of WithTimeout is as follows:
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc)
WithTimeout returns WithDeadline(parent, time.Now().Add(timeout)).

Canceling this context will release its related resources, so the code should call cancel immediately after the operation running in this context is completed, which is usually used for timeout control of database or network connection. Specific examples are as follows:

var wg sync.WaitGroup

func main() {
	// Set a timeout of 50 milliseconds
	ctx, cancel := context.WithTimeout(context.Background(), 50 * time.Millisecond)
	wg.Add(1)
	go worker(ctx)
	time.Sleep(5 * time.Second)
	cancel()  // Notify the child goroutine to end
	wg.Wait()
	fmt.Println("over")
}

func worker(ctx context.Context) {
	loop:
	for {
		fmt.Println("db conncting")
		time.Sleep(time.Millisecond * 10)
		select {
		case <-ctx.Done():  // Automatic call after 50 ms
			break loop
		default:
		}
	}
	fmt.Println("worker done")
	wg.Done()
}

WithValue

The WithValue function can establish a relationship between the data of the request scope and the Context object. The statement is as follows:
func WithValue(parent Context, key, val interface{}) Context
WithValue returns the copy of the parent node, where the value associated with the key is val.

Use the context value only for the data passing the request domain between the API and the process, rather than using it to pass optional parameters to the function.

The keys provided must be comparable and should not be of type string or any other built-in type to avoid conflicts between packages using context. Users of WithValue should define their own types for keys. To avoid allocation when assigned to interface {}, context keys usually have a concrete type struct {}. Alternatively, the static type of the exported context critical variable should be a pointer or interface.

var wg sync.WaitGroup
type TraceCode string

func main() {
	// Set a timeout of 50 milliseconds
	ctx, cancel := context.WithTimeout(context.Background(), 50 * time.Millisecond)
	// Set the trace code in the entry of the system and pass it to the subsequent goroutine to realize log data aggregation
	ctx = context.WithValue(ctx, TraceCode("trace_code"), "123456789")
	wg.Add(1)
	go worker(ctx)
	time.Sleep(5 * time.Second)
	cancel()  // Notify the child goroutine to end
	wg.Wait()
	fmt.Println("over")
}

func worker(ctx context.Context) {
	key := TraceCode("trace_code")
	traceCode, ok := ctx.Value(key).(string)  // Get trace in the sub goroutine_ code
	if !ok {
		fmt.Println("invalid trace code")
	}

	loop:
	for {
		fmt.Println("worker trace code: ", traceCode)
		time.Sleep(time.Millisecond * 10)
		select {
		case <-ctx.Done():  // Automatic call after 50 ms
			break loop
		default:
		}
	}
	fmt.Println("worker done")
	wg.Done()
}

Precautions for using context

  • It is recommended to display and pass Context in the form of parameters
  • For function methods with Context as parameter, Context should be the first parameter.
  • When passing Context to a function method, do not pass nil. If you do not know what to pass, use context.TODO()
  • The Value related method of Context should pass the necessary data of the request field and should not be used to pass optional parameters
  • Context is thread safe and can be safely passed in multiple goroutine s

Client timeout cancellation example

How to implement timeout control on the client when calling the server API?

server side

Click to view the code
// context_timeout/server/main.go
package main

import (
	"fmt"
	"math/rand"
	"net/http"

	"time"
)

// server side, random slow response

func indexHandler(w http.ResponseWriter, r *http.Request) {
	number := rand.Intn(2)
	if number == 1 {
		time.Sleep(time.Second * 10) // 10 second slow response
		fmt.Fprintf(w, "slow response")
		return
	}
	fmt.Fprint(w, "quick response")
}

func main() {
	http.HandleFunc("/", indexHandler)
	err := http.ListenAndServe(":8000", nil)
	if err != nil {
		panic(err)
	}
}

client side

Click to view the code ``` // context_timeout/client/main.go package main

import (
"context"
"fmt"
"io/ioutil"
"net/http"
"sync"
"time"
)

//Client

type respData struct {
resp *http.Response
err error
}

func doCall(ctx context.Context) {
transport := http.Transport{
//Frequent requests can define global client objects and enable long links
//Request infrequent use of short links
DisableKeepAlives: true,
}
client := http.Client{
Transport: &transport,
}

respChan := make(chan *respData, 1)
req, err := http.NewRequest("GET", "http://127.0.0.1:8000/", nil)
if err != nil {
	fmt.Printf("new requestg failed, err:%v\n", err)
	return
}
req = req.WithContext(ctx) // Create a new client request using ctx with timeout
var wg sync.WaitGroup
wg.Add(1)
defer wg.Wait()
go func() {
	resp, err := client.Do(req)
	fmt.Printf("client.do resp:%v, err:%v\n", resp, err)
	rd := &respData{
		resp: resp,
		err:  err,
	}
	respChan <- rd
	wg.Done()
}()

select {
case <-ctx.Done():
	//transport.CancelRequest(req)
	fmt.Println("call api timeout")
case result := <-respChan:
	fmt.Println("call server api success")
	if result.err != nil {
		fmt.Printf("call server api failed, err:%v\n", result.err)
		return
	}
	defer result.resp.Body.Close()
	data, _ := ioutil.ReadAll(result.resp.Body)
	fmt.Printf("resp:%v\n", string(data))
}

}

func main() {
//Define a timeout of 100 milliseconds
ctx, cancel := context.WithTimeout(context.Background(), time.Millisecond*100)
defer cancel() / / call cancel to release the child goroutine resource
doCall(ctx)
}

</details>

Added by smilley654 on Fri, 03 Dec 2021 02:34:13 +0200