aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorSameer Ajmani <sameer@golang.org>2014-07-28 21:01:30 -0400
committerSameer Ajmani <sameer@golang.org>2014-07-28 21:01:30 -0400
commitcb9c80758ce4ef49c450ec2e289d200422c30018 (patch)
tree35360239ff9d3b194769d0f65655287feca2e42c
parent5f4149a2f88286f58e7e043291075422c9686f6d (diff)
go.blog/context: an article about go.net/context.Context.
Blog demo (internal): http://olivia.nyc.corp.google.com:8081/context Server demo (internal): http://olivia.nyc.corp.google.com:8080/search?q=golang&timeout=1s LGTM=bcmills, adg, r R=r, rsc, adg, bcmills, ken, adonovan, dsymonds, crawshaw, campoy, hakim, dneil https://golang.org/cl/116820044
-rw-r--r--content/context.article224
-rw-r--r--content/context/google/google.go88
-rw-r--r--content/context/gorilla/gorilla.go47
-rw-r--r--content/context/interface.go44
-rw-r--r--content/context/server/server.go98
-rw-r--r--content/context/tomb/tomb.go22
-rw-r--r--content/context/userip/userip.go38
7 files changed, 561 insertions, 0 deletions
diff --git a/content/context.article b/content/context.article
new file mode 100644
index 0000000..b4f860b
--- /dev/null
+++ b/content/context.article
@@ -0,0 +1,224 @@
+Go Concurrency Patterns: Context
+23 Jul 2014
+Tags: concurrency, cancelation, cancellation, context
+
+Sameer Ajmani
+
+* Introduction
+
+In Go servers, each incoming request is handled in its own goroutine.
+Request handlers often start additional goroutines to access backends such as
+databases and RPC services.
+The set of goroutines working on a request typically needs access to
+request-specific values such as the identity of the end user, authorization
+tokens, and the request's deadline.
+When a request is canceled or times out, all the goroutines working on that
+request should exit quickly so the system can reclaim any resources they are
+using.
+
+At Google, we developed a `context` package that makes it easy to pass
+request-scoped values, cancelation signals, and deadlines across API boundaries
+to all the goroutines involved in handling a request.
+The package is publicly available as
+[[http://godoc.org/code.google.com/p/go.net/context][code.google.com/p/go.net/context]].
+This article describes how to use the package and provides a complete working
+example.
+
+* Context
+
+The core of the `context` package is the `Context` type:
+
+.code context/interface.go /A Context/,/^}/
+
+(This description is condensed; the
+[[http://godoc.org/code.google.com/p/go.net/context][godoc]] is authoritative.)
+
+The `Done` method returns a channel that acts as a cancelation signal to
+functions running on behalf of the `Context`: when the channel is closed, the
+functions should abandon their work and return.
+The `Err` method returns an error indicating why the `Context` was canceled.
+The [[/pipelines][Pipelines and Cancelation]] article discusses the `Done`
+channel idiom in more detail.
+
+A `Context` does _not_ have a `Cancel` method for same reason the `Done` channel
+is receive-only: the function receiving a cancelation signal is usually not the
+one that sends the signal.
+In particular, when a parent operation starts goroutines for sub-operations,
+those sub-operations should not be able to cancel the parent.
+Instead, the `WithCancel` function (described below) provides a way to cancel a
+new `Context` value.
+
+A `Context` is safe for simultaneous use by multiple goroutines.
+Code can pass a single `Context` to any number of goroutines and cancel that
+`Context` to signal all of them.
+
+The `Deadline` method allows functions to determine whether they should start
+work at all; if too little time is left, it may not be worthwhile.
+Code may also use a deadline to set timeouts for I/O operations.
+
+`Value` allows a `Context` to carry request-scoped data.
+That data must be safe for simultaneous use by multiple goroutines.
+
+** Derived contexts
+
+The `context` package provides functions to _derive_ new `Context` values from
+existing ones.
+These values form a tree: when a `Context` is canceled, all `Contexts` derived
+from it are also canceled.
+
+`Background` is the root of any `Context` tree; it is never canceled:
+
+.code context/interface.go /Background returns/,/func Background/
+
+`WithCancel` and `WithTimeout` return derived `Context` values that can be
+canceled sooner than the parent `Context`.
+The `Context` associated with an incoming request is typically canceled when the
+request handler returns.
+`WithCancel` is also useful for canceling redundant requests when using multiple
+replicas.
+`WithTimeout` is useful for setting a deadline on requests to backend servers:
+
+.code context/interface.go /WithCancel/,/func WithTimeout/
+
+`WithValue` provides a way to associate request-scoped values with a `Context`:
+
+.code context/interface.go /WithValue/,/func WithValue/
+
+The best way to see how to use the `context` package is through a worked
+example.
+
+* Example: Google Web Search
+
+Our example is an HTTP server that handles URLs like
+`/search?q=golang&timeout=1s` by forwarding the query "golang" to the
+[[https://developers.google.com/web-search/docs/][Google Web Search API]] and
+rendering the results.
+The `timeout` parameter tells the server to cancel the request after that
+duration elapses.
+
+The code is split across three packages:
+
+- [[context/server/server.go][server]] provides the `main` function and the handler for `/search`.
+- [[context/userip/userip.go][userip]] provides functions for extracting a user IP address from a request and associating it with a `Context`.
+- [[context/google/google.go][google]] provides the `Search` function for sending a query to Google.
+
+** The server program
+
+The [[context/server/server.go][server]] program handles requests like
+`/search?q=golang` by serving the first few Google search results for `golang`.
+It registers `handleSearch` to handle the `/search` endpoint.
+The handler creates an initial `Context` called `ctx` and arranges for it to be
+canceled when the handler returns.
+If the request includes the `timeout` URL parameter, the `Context` is canceled
+automatically when the timeout elapses:
+
+.code context/server/server.go /func handleSearch/,/defer cancel/
+
+The handler extracts the query from the request and extracts the client's IP
+address by calling on the `userip` package.
+The client's IP address is needed for backend requests, so `handleSearch`
+attaches it to `ctx`:
+
+.code context/server/server.go /Check the search query/,/userip.NewContext/
+
+The handler calls `google.Search` with `ctx` and the `query`:
+
+.code context/server/server.go /Run the Google search/,/elapsed/
+
+If the search succeeds, the handler renders the results:
+
+.code context/server/server.go /resultsTemplate/,/}$/
+
+** Package userip
+
+The [[context/userip/userip.go][userip]] package provides functions for
+extracting a user IP address from a request and associating it with a `Context`.
+A `Context` provides a key-value mapping, where the keys and values are both of
+type `interface{}`.
+Key types must support equality, and values must be safe for simultaneous use by
+multiple goroutines.
+Packages like `userip` hide the details of this mapping and provide
+strongly-typed access to a specific `Context` value.
+
+To avoid key collisions, `userip` defines an unexported variable `key` and uses
+its address as the context key:
+
+.code context/userip/userip.go /var key/
+
+`FromRequest` extracts a `userIP` value from an `http.Request`:
+
+.code context/userip/userip.go /func FromRequest/,/}/
+
+`NewContext` returns a new `Context` that carries a provided `userIP` value:
+
+.code context/userip/userip.go /func NewContext/,/}/
+
+`FromContext` extracts a `userIP` from a `Context`:
+
+.code context/userip/userip.go /func FromContext/,/}/
+
+** Package google
+
+The [[context/google/google.go][google.Search]] function makes an HTTP request
+to the [[https://developers.google.com/web-search/docs/][Google Web Search API]]
+and parses the JSON-encoded result.
+It accepts a `Context` parameter `ctx` and returns immediately if `ctx.Done` is
+closed while the request is in flight.
+
+The Google Web Search API request includes the search query and the user IP as
+query parameters:
+
+.code context/google/google.go /func Search/,/q.Encode/
+
+`Search` uses a helper function, `httpDo`, to issue the HTTP request and cancel
+it if `ctx.Done` is closed while the request or response is being processed.
+`Search` passes a closure to `httpDo` handle the HTTP response:
+
+.code context/google/google.go /var results/,/return results/
+
+The `httpDo` function runs the HTTP request and processes its response in a new
+goroutine.
+It cancels the request if `ctx.Done` is closed before the goroutine exits:
+
+.code context/google/google.go /func httpDo/,/^}/
+
+* Adapting code for Contexts
+
+Many server frameworks provide packages and types for carrying request-scoped
+values.
+We can define new implementations of the `Context` interface to bridge between
+code using existing frameworks and code that expects a `Context` parameter.
+
+For example, Gorilla's
+[[http://www.gorillatoolkit.org/pkg/context][github.com/gorilla/context]]
+package allows handlers to associate data with incoming requests by providing a
+mapping from HTTP requests to key-value pairs.
+In [[context/gorilla/gorilla.go][gorilla.go]], we provide a `Context`
+implementation whose `Value` method returns the values associated with a
+specific HTTP request in the Gorilla package.
+
+Other packages have provided cancelation support similar to `Context`.
+For example, [[http://godoc.org/gopkg.in/tomb.v2][Tomb]] provides a `Kill`
+method that signals cancelation by closing a `Dying` channel.
+`Tomb` also provides methods to wait for those goroutines to exit, similar to
+`sync.WaitGroup`.
+In [[context/tomb/tomb.go][tomb.go]], we provide a `Context` implementation that
+is canceled when either its parent `Context` is canceled or a provided `Tomb` is
+killed.
+
+* Conclusion
+
+At Google, we require that Go programmers pass a `Context` parameter as the
+first argument to every function on the call path between incoming and outgoing
+requests.
+This allows Go code developed by many different teams to interoperate well.
+It provides simple control over timeouts and cancelation and ensures that
+critical values like security credentials transit Go programs properly.
+
+Server frameworks that want to build on `Context` should provide implementations
+of `Context` to bridge between their packages and those that expect a `Context`
+parameter.
+Their client libraries would then accept a `Context` from the calling code.
+By establishing a common interface for request-scoped data and cancelation,
+`Context` makes it easier for package developers to share code for creating
+scalable services.
diff --git a/content/context/google/google.go b/content/context/google/google.go
new file mode 100644
index 0000000..7f8a984
--- /dev/null
+++ b/content/context/google/google.go
@@ -0,0 +1,88 @@
+// Package google provides a function to do Google searches using the Google Web
+// Search API. See https://developers.google.com/web-search/docs/
+package google
+
+import (
+ "encoding/json"
+ "net/http"
+
+ "code.google.com/p/go.blog/content/context/userip"
+ "code.google.com/p/go.net/context"
+)
+
+// Results is an ordered list of search results.
+type Results []Result
+
+// A Result contains the title and URL of a search result.
+type Result struct {
+ Title, URL string
+}
+
+// Search sends query to Google search and returns the results.
+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()
+
+ // Issue the HTTP request and handle the response. The httpDo function
+ // cancels the request if ctx.Done is closed.
+ 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
+}
+
+// httpDo issues the HTTP request and calls f with the response. If ctx.Done is
+// closed while the request or f is running, httpDo cancels the request, waits
+// for f to exit, and returns ctx.Err. Otherwise, httpDo returns f's error.
+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.
+ tr := &http.Transport{}
+ client := &http.Client{Transport: tr}
+ c := make(chan error, 1)
+ go func() { c <- f(client.Do(req)) }()
+ select {
+ case <-ctx.Done():
+ tr.CancelRequest(req)
+ <-c // Wait for f to return.
+ return ctx.Err()
+ case err := <-c:
+ return err
+ }
+}
diff --git a/content/context/gorilla/gorilla.go b/content/context/gorilla/gorilla.go
new file mode 100644
index 0000000..08267c3
--- /dev/null
+++ b/content/context/gorilla/gorilla.go
@@ -0,0 +1,47 @@
+// Package gorilla provides a go.net/context.Context implementation whose Value
+// method returns the values associated with a specific HTTP request in the
+// github.com/gorilla/context package.
+package gorilla
+
+import (
+ "net/http"
+
+ "code.google.com/p/go.net/context"
+ gcontext "github.com/gorilla/context"
+)
+
+// NewContext returns a Context whose Value method returns values associated
+// with req using the Gorilla context package:
+// http://www.gorillatoolkit.org/pkg/context
+func NewContext(parent context.Context, req *http.Request) context.Context {
+ return &wrapper{parent, req}
+}
+
+type wrapper struct {
+ context.Context
+ req *http.Request
+}
+
+var reqKey struct{}
+
+// Value returns Gorilla's context package's value for this Context's request
+// and key. It delegates to the parent Context if there is no such value.
+func (ctx *wrapper) Value(key interface{}) interface{} {
+ if key == &reqKey {
+ return ctx.req
+ }
+ if val, ok := gcontext.GetOk(ctx.req, key); ok {
+ return val
+ }
+ return ctx.Context.Value(key)
+}
+
+// HTTPRequest returns the *http.Request associated with ctx using NewContext,
+// if any.
+func HTTPRequest(ctx context.Context) (*http.Request, bool) {
+ // We cannot use ctx.(*wrapper).req to get the request because ctx may
+ // be a Context derived from a *wrapper. Instead, we use Value to
+ // access the request if it is anywhere up the Context tree.
+ req, ok := ctx.Value(&reqKey).(*http.Request)
+ return req, ok
+}
diff --git a/content/context/interface.go b/content/context/interface.go
new file mode 100644
index 0000000..38a6323
--- /dev/null
+++ b/content/context/interface.go
@@ -0,0 +1,44 @@
+package context
+
+import "time"
+
+// 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{}
+}
+
+// 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)
+
+// WithValue returns a copy of parent whose Value method returns val for key.
+func WithValue(parent Context, key interface{}, val interface{}) Context
+
+// 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
diff --git a/content/context/server/server.go b/content/context/server/server.go
new file mode 100644
index 0000000..bbab5b0
--- /dev/null
+++ b/content/context/server/server.go
@@ -0,0 +1,98 @@
+// The server program issues Google search requests and demonstrates the use of
+// the go.net Context API. It serves on port 8080.
+//
+// The /search endpoint accepts these query params:
+// q=the Google search query
+// timeout=a timeout for the request, in time.Duration format
+//
+// For example, http://localhost:8080/search?q=golang&timeout=1s serves the
+// first few Google search results for "golang" or a "deadline exceeded" error
+// if the timeout expires.
+package main
+
+import (
+ "html/template"
+ "log"
+ "net/http"
+ "time"
+
+ "code.google.com/p/go.blog/content/context/google"
+ "code.google.com/p/go.blog/content/context/userip"
+ "code.google.com/p/go.net/context"
+)
+
+func main() {
+ http.HandleFunc("/search", handleSearch)
+ log.Fatal(http.ListenAndServe(":8080", nil))
+}
+
+// handleSearch handles URLs like /search?q=golang&timeout=1s by forwarding the
+// query to google.Search. If the query param includes timeout, the search is
+// canceled after that duration elapses.
+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.
+
+ // 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)
+
+ // Run the Google search and print the results.
+ start := time.Now()
+ results, err := google.Search(ctx, query)
+ elapsed := time.Since(start)
+ if err != nil {
+ http.Error(w, err.Error(), http.StatusInternalServerError)
+ return
+ }
+ 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
+ }
+}
+
+var resultsTemplate = template.Must(template.New("results").Parse(`
+<html>
+<head/>
+<body>
+ <ol>
+ {{range .Results}}
+ <li>{{.Title}} - <a href="{{.URL}}">{{.URL}}</a></li>
+ {{end}}
+ </ol>
+ <p>{{len .Results}} results in {{.Elapsed}}; timeout {{.Timeout}}</p>
+</body>
+</html>
+`))
diff --git a/content/context/tomb/tomb.go b/content/context/tomb/tomb.go
new file mode 100644
index 0000000..8a2142f
--- /dev/null
+++ b/content/context/tomb/tomb.go
@@ -0,0 +1,22 @@
+// Package tomb provides a Context implementation that is canceled when either
+// its parent Context is canceled or a provided Tomb is killed.
+package tomb
+
+import (
+ "code.google.com/p/go.net/context"
+ tomb "gopkg.in/tomb.v2"
+)
+
+// NewContext returns a Context that is canceled either when parent is canceled
+// or when t is Killed.
+func NewContext(parent context.Context, t *tomb.Tomb) context.Context {
+ ctx, cancel := context.WithCancel(parent)
+ go func() {
+ select {
+ case <-t.Dying():
+ cancel()
+ case <-ctx.Done():
+ }
+ }()
+ return ctx
+}
diff --git a/content/context/userip/userip.go b/content/context/userip/userip.go
new file mode 100644
index 0000000..3001ade
--- /dev/null
+++ b/content/context/userip/userip.go
@@ -0,0 +1,38 @@
+// Package userip provides functions for extracting a user IP address from a
+// request and associating it with a Context.
+package userip
+
+import (
+ "fmt"
+ "net"
+ "net/http"
+ "strings"
+
+ "code.google.com/p/go.net/context"
+)
+
+// FromRequest extracts the user IP address from req, if present.
+func FromRequest(req *http.Request) (net.IP, error) {
+ s := strings.SplitN(req.RemoteAddr, ":", 2)
+ userIP := net.ParseIP(s[0])
+ if userIP == nil {
+ return nil, fmt.Errorf("userip: %q is not IP:port", req.RemoteAddr)
+ }
+ return userIP, nil
+}
+
+// The address &key is the context key.
+var key struct{}
+
+// NewContext returns a new Context carrying userIP.
+func NewContext(ctx context.Context, userIP net.IP) context.Context {
+ return context.WithValue(ctx, &key, userIP)
+}
+
+// FromContext extracts the user IP address from ctx, if present.
+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(&key).(net.IP)
+ return userIP, ok
+}