Author: Sameer Ajmani | address: blog.golang.org/context

The translator preface

The second official blog post is about the Go concurrency control context package.

Overall, I think the previous article was the foundation and core of Go concurrency. Context is an easy-to-use library developed for Goroutine control, building on the previous chapter. After all, passing only done channels between different Goroutines is really too little information.

This article briefly introduces the methods provided by context and shows you how to use them. Then, through a search example, it introduces the use in a real scenario.

In addition to the official context implementation, there are also some third-party implementations such as github.com/context and Tomb, but these have stopped updating since the official context appeared. The reason is simple. After all, the authorities are usually stronger. Before, go module management is also a hundred flowers, but recently the official launched their own solution, maybe soon, other ways will be obsolete.

Actually, I don’t think this article is easy to read. It doesn’t feel gradual enough. A sudden example can be a little confusing.

The text of the translation is as follows:


In Go’s services, each request is handled by a separate Goroutine, and each Goroutine usually starts a new goroutine to perform additional tasks, such as accessing a database or RPC service. The Goroutine within the request needs to be able to share access to request data, such as user authentication, authorization token, and request expiration time. If the request is cancelled or a timeout occurs, all goroutines in the range of the request should exit immediately for resource reclamation.

At Google, we developed a context package that makes it very easy to pass request data, cancel signals, and timeout messages between goroutines within a request. See context for details.

This article describes the use of the Context package in detail and provides a complete use case.

Context

The core of context is the context type. The definition is as follows:

// A deadline, cancellation signal, and request-scoped values
// across API. Its methods are safe for simultaneous use by multiple goroutines
// A Context can pass expiration dates, cancel signals, request data between apis (whether coroutines or not).
// Methods in Context are coroutine safe.
type Context interface {
    // Done returns a channel that is closed when this context is cancelled
    // or times out.
    // The Done method returns a channel, and when the context cancels or times out, the Done method closes.
    Done() <-chan struct{}

    // Err indicates why this context was canceled, after the Done channel
    // is closed
    // After Done is closed, Err can be used to indicate why the context was cancelled
    Err() error

    // Deadline returns the time when this Context will be canceled, if any.
    // Cancel the context when it expires
    Deadline() (deadline time.Time, ok bool)

    // Value returns the value associated with key or nil if none
    Value(key interface{}) interface{}}Copy the code

The introduction is brief, see GoDoc for details.

The Done method returns a channel that can be used to receive cancellation signals from the context. When a channel is closed, the function listening for the Done signal immediately abandons the current work and returns. The Err method returns an error variable from which you can tell why the context was cancelled. Pipeline and Cancelation introduces Done channel in detail.

The reason why the Context does not have a cancel method is similar to the reason why the Done Channel is read-only: the goroutine that receives the cancel signal is not responsible for canceling. In particular, when the parent starts the child Goroutine to perform the operation, the child cannot cancel the parent. Instead, the WithCancel method (described next) provides a way to cancel the newly created Context.

Context is coroutine concurrency safe. We can pass the Context to any number of Goroutines, and we can signal all of them with Cancel.

The Deadline method lets the function decide if it needs to start the work, and if the remaining time is too short, the work is not worth it. In code, we can set a timeout for IO operations via deadline.

The Value method enables the context to share request-scoped data between goroutines, which needs to be coroutine concurrency safe.

Derived the Context

The context package provides multiple functions to derive a new context from an existing context instance. These contexts will form a tree structure, and as soon as a Context is cancelled, all derived contexts will be cancelled.

The Background function returns the Context as any Context root and cannot be cancelled.

// Background returns an empty Context. It is never canceled, has no deadline,
// and has no values. Background is typically used inMain, init, and tests, // and as the top-level ContextforThe incoming requests. // Background function returns an empty Context that cannot be cancelled, has no deadlines, and has no shared data. Background is only used in main, init, or tests functions. func Background() ContextCopy the code

WithCancel and WithTimeout give birth to a new Context instance, and the derived instance is cancelled before the parent. The Context instance associated with the request will be cancelled after the request processing is complete. WithCancel can be used to cancel redundant requests when multiple copies of data are requested. WithTimeout can be used to set the timeout when requesting a back-end service.

// WithCancel returns a copy of parent whose Done channel is closed as soon as
// parent.Done is closed or cancel is called.
// WithCanal returns a copy of the parent Context. When the parent's Done channel is closed or cancels, its Done channel is also closed.
func WithCancel(parent Context) (ctx Context, cancel CancelFunc)

// A CancelFunc cancels a Context. / /CancelFuncUsed to cancelContext
type CancelFunc func(a)

// WithTimeout returns a copy of parent whose Done channel is closed as soon as
// parent.Done is closed.cancel is called.or timeout elapses. The new
// Context's Deadline is the sooner of now+timeout and the parent's deadline.if
// any. If the timer is still running.the cancel function releases its
// resources. // Returns the parentContextCopies andCancelFuncThree cases of itsDoneWill close, respectively parentDoneShut down,cancelIs called, and reaches the timeout.func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc)
Copy the code

WithValue provides a way to pass request-related data through Context

// WithValue returns a copy of parent whose Value method returns val for key.
func WithValue(parent Context, key interface{}, val interface{}) Context
Copy the code

How do you use context? The best way to do this is through a case study.

Example: Google Web search

Demonstrate a case that implements an HTTP service that handles things like /search? Q =golang&timeout=1s request. Timeout: If the request processing time exceeds the specified time, the execution will be canceled.

The code mainly involves three packages, which are as follows:

  • Server, main function entry and /search handler;
  • Userip, which implements a public function to export user IP from the request context;
  • Google, implemented the Search function, is responsible for sending Search requests to Google;

Start introducing!

Package server

Server handles things like /search? HandleSearch is the actual handler that first initializes a Context, named CTX, and implements the function to exit cancel by defer. If the request parameter contains timeout, create a context with WithTimeout. After timeout, the context is automatically cancelled.

func handleSearch(w http.ResponseWriter, req *http.Request) {
    // ctx is the Context for this handler. Calling cancel closes the
    // cxt.Done channel, which is the cancellation signal for requests
    // started by this handler
    var (
        ctx context.Context
        cancel context.Context
    )

    timeout, err := time.ParseDuration(req.FromValue("timeout"))
    iferr ! =nil {
        // the request has a timeout, so create a context that is
        // canceled automatically when the timeout expires.
        ctx, cancel = context.WithTimeout(context.Background(), timeout)
    } else {
        ctx, cancel = context.WithCancel(context.Background())
    }
    defer cancel() // Cancel ctx as soon as handlSearch returns.
Copy the code

Next, the handler retrieves the query keyword and client IP from the request by calling the userIP package function. At the same time, the client IP is attached to CTX because the request for the back-end service also requires it.

    // Check the search query
    query := req.FormValue("q")
    if query == "" {
        http.Error(w, "no query", http.StatusBadRequest)
        return
    }

    // Store the user IP in ctx for use by code in other packages.
    userIP, err := userip.FormRequest(req)
    iferr ! =nil {
        http.Error(w, e.Error(), http.StatusBadRequest)
        return
    }

    ctx = userip.NewContext(ctx, userIP)
Copy the code

Call Google.search with CTX and query parameters.

    // Run the Google search and print the results
    start := time.Now()
    results, err := google.Search(ctx, query)
    elapsed := time.Since(start)
Copy the code

After the search is successful, handler renders the resulting page.

    if err := resultsTemplate.Execute(w, struct{ Results google.Results Timeout, Elapsed time.Duration }{ Results: results, Timeout: timeout, Elaplsed: elaplsed, }); err ! =nil {
        log.Print(err)
        return
    }
Copy the code

Package userip

The userip package provides two functions that export the userip from the request and bind the userip to the Context. Context contains key-value mappings. Both key and value are of type interface{}. Key must support equality comparison, and value must be concurrency safe for coroutines. The UserIP package hides map details by performing a type conversion on the value of the Context, the client IP. To avoid key conflicts, userip defines a key of type that cannot be exported.

// The key type is unexported to prevent collision with context keys defined in
// other package
type key int

// userIPkey is the context key for the user IP address. Its value of zero is
// arbitrary. If this package defined other context keys, they would have
// different integer values.
const userIPKye key = 0
Copy the code

The FromRequest function is responsible for exporting user IP from http.Request:

func FromRequest(req *http.Request) (net.IP, error) {
    ip, _, err := net.SplitHostPort(req.RemoteAddr)
    iferr ! =nil {
        return nil, fmt.Errorf("userip: %q is not IP:port", req.RemoteAddr)
    }
Copy the code

The NewContext function generates a Context with userIP:

func NewContext(ctx context.Context, userIP net.IP) context.Context {
    return context.WithValue(ctx, userIPKey, userIP)
}
Copy the code

FromContext is responsible for exporting userIP FromContext:

func FromContext(ctx context.Context) (net.IP. bool) {
    // ctx.Value returns nil if ctx has no value for the key;
    // the net.IP type assertion returns ok=false for nil
    userIP, ok := ctx.Value(userIPKey).(net.IP)
    return userIP, ok
}
Copy the code

Package google

Google.search is responsible for the request of the Google Web Search interface and the parsing of JSON data returned by the interface. It receives the Context type CTX argument, and if ctx.Done is turned off, it will return immediately even if the request is running.

The query request parameters include the Query keyword and the user IP address.

func Search(ctx context.Context, query string) (Results, error) {
    // Prepare the Google Search API request
    req, err := http.NewRequest("GET"."http://ajax.googleapis.com/ajax/services/search/web?v=1.0".nil)
    iferr ! =nil {
        return nil, err
    }
    q := req.URL.Query()
    q.Set("q", query)

    // If ctx is carrying the user IP address, forward it to the server
    // Google APIs use the user IP to distinguish server-initiated requests 
    // from end-users requests
    if userIP, ok := userip.FromContext(ctx); ok {
        q.Set("userip", userIP.String())
    }
    req.URL.RawQuery = q.Encode()
Copy the code

The Search function uses a helper function, httpDo, which is responsible for initiating HTTP requests. If ctx.done is closed, the request is closed even if it is being executed. Search passes a closure function to httpDo to process the result of the response.

    var results Results
    err = httpDo(ctx, req, func(resp *http.Response, err error) error {
        iferr ! =nil {
            return err
        }
        defer resp.Body.Close()

        // Parse the JSON search result.
        // https://developers.google.com/web-search/docs/#fonje
        var data struct {
            ResponseData struct {
                Results []struct {
                    TitleNoFormatting string
                    URL               string}}}iferr := json.NewDecoder(resp.Body).Decode(&data); err ! =nil {
            return err
        }

        for _, res := range data.ResponseData.Results {
            results = append(results, Result{Title: res.TitleNoFormatting, URL: res.URL})
        }

        return nil
    })

    return results, err
Copy the code

The httpDo function opens a new Goroutine responsible for HTTP request execution and response result processing. If the request is not completed before Goroutine exits, the request will be cancelled if ctx.done is closed.

func httpDo(ctx context.Context, req *http.Request, f func(*http.Request, error) error) error {
    // Run the HTTP request in a goroutine and pass the response to f.
    c := make(chan error, 1)
    req := req.WithContext(ctx)
    go func(a) { c <- f(http.DefaultClient.Do(req)) }()
    select {
    case <-ctx.Done():
        <- c
        return ctx.Err
    case  err := <-c:
        return err
    }
}
Copy the code

Adjust the code based on the Context

Many server-side frameworks provide packages and data types for requested data delivery. We can write new implementation code based on the Context interface to complete the connection between the framework and the handler function.

Translator's note: The following are third-party implementations of the two contexts described by the developer, some of which require a brief understanding of them to fully understand.Copy the code

For example, Gorilla’s Context implements associated data binding by providing key value mappings on requests. In Gorilla. Go, we provide an implementation of the Context, whose Value method returns a Value associated with a specific HTTP request.

Some other packages provide cancellation support similar to Context. For example, the Kill method in Tomb can realize cancellation signal by closing Dying Channel. Tomb also provides a method for waiting for goroutine to exit, similar to sync.waitgroup. In tomb. Go, there is an implementation that cancels the current Context when the parent Context is cancelled or tomb is killed.

conclusion

At Google, for functions that receive or send requests, we require that Context be passed as the first argument. This way, Go code from different teams can work well. Context is very convenient for goroutine timeouts and uncontrols, as well as ensuring the secure delivery of important data, such as security credentials.

The context-based service framework needs to implement the Context to help connect the framework to the consumer, who expects to receive Context parameters from the framework. The client library, on the other hand, receives the Context argument from the caller. Context allows package developers to easily share their code and build more scalable services by establishing a common interface for requesting data and canceling control.