Golang standard library context Context

Posted by phpian on Sun, 06 Mar 2022 11:58:24 +0100

Golang standard library context Context

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 data specific to the request, such as the identity authentication information of the end user, the token related to authentication, 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.

1 Why do I need Context

Basic example

package main

import (
    "fmt"
    "sync"

    "time"
)

var wg sync.WaitGroup

// Initial example

func worker() {
    for {
        fmt.Println("worker")
        time.Sleep(time.Second)
    }
    // How to receive external commands to exit
    wg.Done()
}

func main() {
    wg.Add(1)
    go worker()
    // How to gracefully implement the ending sub goroutine
    wg.Wait()
    fmt.Println("over")
}

Global variable mode

package main

import (
    "fmt"
    "sync"

    "time"
)

var wg sync.WaitGroup
var exit bool

// Problems with global variable mode:
// 1. Using global variables is not easy to unify when calling across packages
// 2. If goroutine is started again in the worker, it is not easy to control.

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

func main() {
    wg.Add(1)
    go worker()
    time.Sleep(time.Second * 3) // Sleep for 3 seconds to prevent the program from exiting too quickly
    exit = true                 // Modify the global variable to realize the exit of the sub goroutine
    wg.Wait()
    fmt.Println("over")
}

Channel mode

package main

import (
    "fmt"
    "sync"

    "time"
)

var wg sync.WaitGroup

// 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 worker(exitChan chan struct{}) {
LOOP:
    for {
        fmt.Println("worker")
        time.Sleep(time.Second)
        select {
        case <-exitChan: // Waiting for notification from superior
            break LOOP
        default:
        }
    }
    wg.Done()
}

func main() {
    var exitChan = make(chan struct{})
    wg.Add(1)
    go worker(exitChan)
    time.Sleep(time.Second * 3) // Sleep for 3 seconds to prevent the program from exiting too quickly
    exitChan <- struct{}{}      // Send exit signal to sub goroutine
    close(exitChan)
    wg.Wait()
    fmt.Println("over")
}

Official version of the scheme

package main

import (
    "context"
    "fmt"
    "sync"

    "time"
)

var wg sync.WaitGroup

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

func main() {
    ctx, cancel := context.WithCancel(context.Background())
    wg.Add(1)
    go worker(ctx)
    time.Sleep(time.Second * 3)
    cancel() // Notify the child goroutine to end
    wg.Wait()
    fmt.Println("over")
}

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

package main

import (
    "context"
    "fmt"
    "sync"

    "time"
)

var wg sync.WaitGroup

func worker(ctx context.Context) {
    go worker2(ctx)
LOOP:
    for {
        fmt.Println("worker")
        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("worker2")
        time.Sleep(time.Second)
        select {
        case <-ctx.Done(): // Waiting for superior notice
            break LOOP
        default:
        }
    }
}
func main() {
    ctx, cancel := context.WithCancel(context.Background())
    wg.Add(1)
    go worker(ctx)
    time.Sleep(time.Second * 3)
    cancel() // Notify the child goroutine to end
    wg.Wait()
    fmt.Println("over")
}

2. Context 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.

3. 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;

4. 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 tree structure of Context, 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 does not carry any value.

5. With series functions

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

6. 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 code should call cancel immediately after the operation running in this context is completed.

func gen(ctx context.Context) <-chan int {
        dst := make(chan int)
        n := 1
        go func() {
            for {
                select {
                case <-ctx.Done():
                    return // return end the goroutine to prevent leakage
                case dst <- n:
                    n++
                }
            }
        }()
        return dst
    }
func main() {
    ctx, cancel := context.WithCancel(context.Background())
    defer cancel() // When we get the required integer, we call cancel.

    for n := range gen(ctx) {
        fmt.Println(n)
        if n == 5 {
            break
        }
    }
}

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.

7. WithDeadline

The function signature of WithDeadline is as follows:

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

Return a copy of the parent context and adjust the deadline to 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 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(1 * time.Second):
        fmt.Println("overslept")
    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) gets a context (ctx) and a cancel function (cancel), and then uses a select to put the main program into a wait: wait for 1 second and print overslept to exit, or wait for ctx to expire and exit. Because ctx expires in 50 seconds, ctx Done () will receive the value first, and the above code will print ctx Err () cancellation reason.

8. 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:

package main

import (
    "context"
    "fmt"
    "sync"

    "time"
)

// context.WithTimeout

var wg sync.WaitGroup

func worker(ctx context.Context) {
LOOP:
    for {
        fmt.Println("db connecting ...")
        time.Sleep(time.Millisecond * 10) // Suppose it takes 10 milliseconds to connect to the database normally
        select {
        case <-ctx.Done(): // Automatic call after 50 milliseconds
            break LOOP
        default:
        }
    }
    fmt.Println("worker done!")
    wg.Done()
}

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

9. 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. In order to avoid allocation when assigned to interface {}, the context key usually has the specific type struct {}. Alternatively, the static type of the exported context critical variable should be a pointer or interface.

package main

import (
    "context"
    "fmt"
    "sync"

    "time"
)

// context.WithValue

type TraceCode string

var wg sync.WaitGroup

func worker(ctx context.Context) {
    key := TraceCode("TRACE_CODE")
    traceCode, ok := ctx.Value(key).(string) // Get the trace code in the sub goroutine
    if !ok {
        fmt.Println("invalid trace code")
    }
LOOP:
    for {
        fmt.Printf("worker, trace code:%s\n", traceCode)
        time.Sleep(time.Millisecond * 10) // Suppose it takes 10 milliseconds to connect to the database normally
        select {
        case <-ctx.Done(): // Automatic call after 50 milliseconds
            break LOOP
        default:
        }
    }
    fmt.Println("worker done!")
    wg.Done()
}

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

10. 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 don't 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

11. Example of client timeout cancellation

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

12. server side

// 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 == 0 {
        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

// 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)
}

Topics: Go Back-end