Go language concurrent mode: Context package

This paper is translated from Sameer Ajmani's Go Concurrency Patterns: Context.

Catalog

Explanation and use of Go language Context package

introduce

context

Derived contexts

Example: Google Web Search

Server program

Package userip

Package google

Adjust code to context

conclusion

 

Explanation and use of Go language Context package

introduce

In the Go server, each arriving request is processed by the corresponding goroutine. The request handler adds new goroutine to access the backend, such as database and RPC services. The goroutine set that jointly serves a request needs to obtain specific request information, such as the identity of the end user, the authentication identity and the deadline of the requester. When a request is cancelled or expired, all related goroutines need to exit quickly to release system resources in time.

At Google, we developed a package called context, which can deliver values, cancellations, and deadlines within a specific request range to all goroutine s that process the request. This Bao Gongkai is published as context. This article describes how to use this package and provides a complete working 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.
// A Context encapsulates the deadline, cancellation signal and specific request information, which can be used through the API. Its method can be safely
// Multiple goroutine s are used at the same time.
type Context interface {
    // Done returns a channel that is closed when this Context is canceled
    // or times out.
    // When the Context is cancelled or obsolete, Done returns a closed channel.
    Done() <-chan struct{}

    // Err indicates why this context was canceled, after the Done channel
    // is closed.
    // When Done's channel is closed, Err indicates why the context is cancelled.
    Err() error

    // Deadline returns the time when this Context will be canceled, if any.
    // If there is a Deadline, deadlock returns the time when the Context was revoked.
    Deadline() (deadline time.Time, ok bool)

    // Value returns the value associated with key or nil if none.
    // Value returns the data bound to the key. nil if there is no corresponding data.
    Value(key interface{}) interface{}
}

The Done method returns a channel as a cancel signal to Context related functions: when the channel is closed, these functions need to give up work and return.

The Err method returns an error indicating why the context was cancelled. Article Pipeline and cancel detail To the discussion channel idiom Done.

Context has no cancellation method, for the same reason, the channel returned by Done can only be used for receiving: generally, the function receiving the cancellation signal is not the function sending the signal. Specifically, after a parent operation enables goroutine for a child operation, the child operation cannot cancel the parent operation. WithCancel provides a way to cancel context.

Context can be safely used by multiple goroutines at the same time. We can send the context to any number of goroutines, and then notify all goroutines by canceling the context.

The deadline method helps the function determine if it is working; if there is too little time left, it may not be worth starting. The code can use deadline to set the time limit for I/O operations.

Value allows a Context to load data for a specific request. These data must be able to be safely used by multiple goroutine s at the same time.

Derived contexts

Context provides functions to derive new from existing context. These contexts form a tree: when the predecessor context is cancelled, all the descendants' contexts are cancelled at the same time.

Background is the root of all Context trees; it can 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.
// Background returns an empty context. It can't be cancelled, there's no deadline, there's no data. Background
// Used in main, init, and tests as the top-level context of the received requests.
func Background() Context

WithCancel and WithTimeout derive new context values, which can be cancelled earlier than the context of the predecessor. Context related to a request is often cancelled after the request processing function returns. When using multiple replicas, WithCancel can be used to cancel redundant requests. WithTimeout can set a deadline on requests sent to back-end servers.

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

// A CancelFunc cancels a Context.
// 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.
// WithTimeout returns a copy of the parent's Done channel, which is closed or cancelled as the parent's Done is closed.
// If there is a cut-off time, the cut-off time of Context is a value closer to the current value of both now+timeout and parent cut-off time.
// If the timer is still running, the cancel function frees its resources.
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc)

WithValue provides a way to connect the data of a specific request with context:

// WithValue returns a copy of parent whose Value method returns val for key.
// WithValue returns a copy of the parent, and the Value method of the child can return val according to the key.
func WithValue(parent Context, key interface{}, val interface{}) Context

The best way to understand the working mechanism of context package is through a practical example.

Example: Google Web Search

Our example is an HTTP server that processes URL s similar to / search? Q = golang & timeout = 1s, first pushes the query request to the Google Web search interface, and then renders the query results. The timeout parameter tells the server the query deadline.

The code is divided into three packages:

  • server provides main function and / search processing function.
  • userip provides the function to extract the user IP address from the request and bind it to Context.
  • Google provides a query function to send requests to Google.

Server program

The server program processes the request and returns the top several Google search results. For example, / search?q=golang, it returns the query results of golang. It registers handleSearch to handle the / search terminal. The handler creates an initial Context called ctx and sets it to cancel when the handler returns. If the request contains a timeout URL parameter, the Context time will be automatically cancelled as soon as it arrives.

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.

The processing function extracts the query task from the request and calls the userip packet to extract the user IP address. The backend request requires a user IP address, so handleSearch adds 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)

The processing function calls google.Search with ctx and query as parameters:

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

If the search is successful, process the function render request:

    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
    }

Package userip

The userip package provides the function to extract the user IP address from the request and bind it to context. Context provides key value mapping. The types of key and value are interface {}. The actual type of key must support equal comparison, and the actual type of value must be able to be safely used by multiple goroutine s at the same time. Packages like userip hide the details of this mapping and provide strongly typed access to specific context values.

In order to avoid key conflicts, userip defines the type key that cannot be accessed externally, and uses the value of this type as the key of context.

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

FromRequest extracted a userIP value from an 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)
    }

NewContext returns a Context containing the userIP value just obtained:

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

FromContext extract userIP from Context:

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
}

Package google

The google.Search function sends an HTTP request to the Google Web search interface and parses the results in JSON format. It takes a Context type parameter ctx. If ctx.Done is turned off, it returns immediately, even if the request is still executing.

Google Web search interface requests to use search query and user IP address as 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()

The Search function uses a helper function, httpDo, to send HTTP requests. If ctx.Done is turned off, it cancels the httpdo even if the request or response is still executing. Search passes a closure to httpdo to process 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, err

The httpDo function performs HTTP requests and opens a new goroutine to handle the response. If ctx.Done is cancelled, it cancels the request before goroutine exits.

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

Adjust code to context

Many server frameworks provide code packages and types for passing specific request data. We can define a new code implementation that conforms to the Context interface to communicate the existing framework and code that requires Context as a parameter.

For example, Gorilla's github.com/gorilla/context The package provides mapping from HTTP requests to key value pairs, allowing processing functions to associate data with received requests. stay gorilla.go , we provide the code implementation of Context, whose Value method returns the data corresponding to the specific HTTP request in the Gorilla package.

Other packages also support cancellation operations similar to Context. For example, Tomb A kill method is provided to deliver cancellation information by closing the channel. Tomb also provides a way to wait for those goroutine s to exit, similar to sync.WaitGroup. In tomb.go In, we provide another implementation of the Context interface. After the predecessor Context is cancelled or a specific Tomb is killed, the Context is also cancelled.

conclusion

In Google, we require the go programmer to take the Context parameter as the first parameter of each function in the call path of receiving and sending requests. This makes the go code developed by different teams compatible. It provides simple obsolescence and cancellation controls to ensure that critical information, such as security certificates, is delivered accurately between Go programs.

In order to adapt to Context, the server framework needs to provide the implementation of Context to communicate their packages and codes that need Context as parameters. Their client code base needs to be able to receive Context from the calling code. In the field of building scalable services, Context makes code sharing easier by establishing a common interface for specific request data and cancellation mechanism.

Published 240 original articles, won praise 106, visited 180000+
Private letter follow

Tags: Google JSON Go Database

Posted on Tue, 10 Mar 2020 06:30:18 -0400 by Bluemercury