In the Go service, each incoming request is processed in a separate Goroutine. The request callback function usually starts additional Goroutines to access the back end, such as databases and RPC services. A series of Goroutines that process the same request typically require access to request-related values, such as the end user’s identity, authorization token, and request expiration time. When a request is cancelled or times out, all goroutines handling the request should exit quickly so that the system can reclaim the resources they are using.

At Google, we developed a context package that easily crosses API boundaries and passes the value of the request scope, cancellation signal, and expiration time to all goroutines that handle the request. The publicly available version of this package is Context. This article describes how to use this package and provides a complete example.

Context

The core of the context package is the context type:

// A Context carries a deadline, cancelation signal, and request-scoped values // across API boundaries. Its methods are safe for simultaneous use by multiple // goroutines.  type Context interface { // Done returns a channel that is closed when this Context is canceled // or times out. Done()  <-chan struct{} // Err indicates why this context was canceled, after the Done channel // is closed. Err() error // Deadline returns the time when this Context will be canceled, if any. Deadline() (deadline time.Time, ok bool) // Value returns the value associated with key or nil if none. Value(key interface{}) interface{} }Copy the code

(This description is concise; Godoc is authoritative.)

The Done method returns a channel that acts as a cancellation signal to the functions running in the Context: when the channel closes, the functions should abandon their work and return. The Err method returns an error indicating the reason for canceling the context. “Done channel” In Charge and Cancelation

Context has no Cancel method, for the same reason that a Done channel is read-only: the function that receives the Cancel signal is not usually the one that sends the signal. In particular, when the parent starts goroutine for the child, the child should not have the ability to cancel the parent. Instead, the WithCancel function (described below) provides a way to cancel the new Context value.

multiplegoroutineSimultaneous use of the sameContextIt’s safe. The code can be separated individuallyContextPass to any number ofgoroutineAnd cancel theContextTo allgoroutineSend a signal.

The Deadline method allows a function to decide if it should start work; If there is too little time left, it may not be worth it. Code can also use cutoff times to set I/O operation timeouts.

Value allows the Context to carry request-scoped data. In order for multiple Goroutines to be used simultaneously, this data must be secure.

Derived contexts

The context package provides functions to derive new context values from existing context values. These values form a tree: when the Context is cancelled, all contexts derived from it are also cancelled.

Background is the root of all Context trees; It will never be cancelled:

// Background returns an empty Context. It is never canceled, has no deadline,
// and has no values. Background is typically used in main, init, and tests,
// and as the top-level Context for incoming requests.
func Background() Context
Copy the code

WithCancel and WithTimeout return derived Context values that can be cancelled earlier than the parent Context. When the request callback function returns, the Context associated with the incoming request is usually cancelled. WithCancel can also be used to cancel redundant requests when multiple copies are used. WithTimeout is used to set the cut-off time for requests to the back-end server:

// WithCancel returns a copy of parent whose Done channel is closed as soon as
// parent.Done is closed or cancel is called.
func WithCancel(parent Context) (ctx Context, cancel CancelFunc)

// A CancelFunc cancels a Context.
type CancelFunc func()

// 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.
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc)
Copy the code

WithValue provides a way to associate a request-scoped value with a 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

See an example of the best way to use the Context package.

Example: Google Web search

Our example is an HTTP server that handles urls such as /search? Q =golang&timeout=1s, forward the query “golang” to the Google Web Search API and render the results. The timeout parameter tells the server to cancel the request after this delay.

The code is divided into three packages:

Server program

Server programs handle things like /search? Q = Golang’s request provides Golang with Google search results. It registers handleSearch to handle /search endpoint. The callback creates an initial Context called CTX and arranges to cancel it when the callback returns. If the request contains the timeout URL argument, the Context is automatically cancelled when the timeout ends:

func handleSearch(w http.ResponseWriter, req *http.Request) {
    // ctx is the Context for this handler. Calling cancel closes the
    // ctx.Done channel, which is the cancellation signal for requests
    // started by this handler.
    var (
        ctx    context.Context
        cancel context.CancelFunc
    )
    timeout, err := time.ParseDuration(req.FormValue("timeout"))
    if err == 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 handleSearch returns.
Copy the code

The callback function extracts the query from the request and extracts the CLIENT’s IP address by calling the userIP package. The back-end request requires the CLIENT’s IP address, so handleSearch appends it to CTX:

// 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.FromRequest(req) if err ! = nil { http.Error(w, err.Error(), http.StatusBadRequest) return } ctx = userip.NewContext(ctx, userIP)Copy the code

The callback calls Google.search using CTX and query:

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

If the search is successful, the callback function renders the result:

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

Package userip

The userIP package provides functions to extract the user’S IP address from the request and associate it with the Context. Context provides key-value mapping, where keys and values are of type interface{}. The key type must support equality, and the value must be safe for simultaneous use by multiple Goroutines. Packages like Userip hide the details of the mapping and provide strongly typed access to specific Context values.

To avoid key collisions, userip defines an unexported type key and uses the value of that type as the Context key:

// The key type is unexported to prevent collisions with context keys defined in
// other packages.
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 userIPKey key = 0
Copy the code

FromRequest extracts the value of userIP from http.Request:

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

NewContext returns a NewContext with the value of the input parameter userIP:

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

FromContext extracts 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

The Google. Search function makes an HTTP request to the Google Web Search API and parses the JSON-encoded results. It accepts the Context parameter CTX and returns immediately if ctx.Done is closed when the request is run.

Google Web Search API requests include Search query and user IP as query parameters:

func Search(ctx context.Context, query string) (Results, error) { // Prepare the Google Search API request. req, Err: = HTTP. NewRequest (" GET ", "https://ajax.googleapis.com/ajax/services/search/web?v=1.0", nil) if err! = 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-user requests. if userIP, ok := userip.FromContext(ctx); ok { q.Set("userip", userIP.String()) } req.URL.RawQuery = q.Encode()Copy the code

Search uses a helper function, httpDo, to make HTTP requests; If ctx.done is off while processing a request or response, the call is canceled. Search passes the closure to httpDo to handle the HTTP response:

var results Results err = httpDo(ctx, req, func(resp *http.Response, err error) error { if err ! = 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 } } } if err := 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 }) // httpDo waits for the closure we provided to return, so it's safe to // read results here. return results, errCopy the code

The httpDo function runs the HTTP request and processes its response in a new Goroutine. If ctx.done is closed before goroutine exits, the request is cancelled:

func httpDo(ctx context.Context, req *http.Request, f func(*http.Response, 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() { c <- f(http.DefaultClient.Do(req)) }()
    select {
    case <-ctx.Done():
        <-c // Wait for f to return.
        return ctx.Err()
    case err := <-c:
        return err
    }
}
Copy the code

Adjust the code to the Context

Many service frameworks provide packages and types that carry the values of the request scope. We can define a new implementation of the Context interface to bridge the gap between code that uses the existing framework and code that requires the Context parameter.

For example, Gorilla’s github.com/gorilla/con… Packages allow handlers to associate data with incoming requests by providing a mapping from HTTP requests to key-value pairs. In Gorilla. Go, we provide a Context implementation whose Value method returns the Value associated with the HTTP request in the Gorilla package.

Other packages provide context-like cancellation support. For example, Tomb provides a Kill method that signals cancellation by closing a Dying channel. Tomb also provides a way to wait for these Goroutines to exit, similar to sync.waitgroup. In tomb.go, we provide a Context implementation that is cancelled when its parent Context is cancelled or when the provided tomb is killed.

conclusion

At Google, we require the Go programmers to pass the Context parameter as the first parameter to each function along the call path between incoming and outgoing requests. This makes Go code developed by many different teams interoperable. It provides simple control over timeouts and cancellations, and ensures that key values such as security credentials are correctly passed into the program.

Server frameworks that want to build based on Context should provide implementations of the Context to bridge the gap between their packages and those that require Context parameters. Their client libraries accept the Context from the calling code. By establishing a common interface for request-scoped data and cancellation, Context makes it easier for package developers to share code to create scalable services.

Post: Go Concurrency Patterns: Context

Author: Cyningsun Author: www.cyningsun.com/01-19-2021/… Copyright notice: All articles on this blog are licensed under CC BY-NC-ND 3.0CN unless otherwise stated. Reprint please indicate the source!