It’s no secret that Go’s main niche is network services: various servers, backends, microservices, distributed databases and file storages. This class of programs very actively uses network requests, all the necessary functionality for which is in the standard library, but one aspect of the development of network architectures remains for many a dark spot - query contexts. In this article I want to consider this aspect more closely and show what a powerful and important tool it is.

What is context?
Let's start with the main question - what kind of concept is this, “context”? The context in this, sorry for the tautology, the context is some information about the object that is passed between the boundaries of the API functions. The object usually refers to a network request, and the bounds of the API refer to various middleware, different packages and layers of abstractions, but the very concept of “context” is not specific only to network requests. But this article will be mainly about them.
A typical example is to extract user data from a JWT token and further forward this information to the handlers for access checking, logging, and so on.
')
This concept of context, at least for network requests, is found in many languages and frameworks — in C # it is
HttpContext , in Java Netty -
ChannelHandlerContext , in Python Twisted -
twisted.python.context, and so on.
Contexts in Go
The standard Go library has an excellent
HTTP stack , which allows you to quickly create multi-threaded servers without fear of
“10K connection problems” and easily implement a wide variety of query processing scripts using the
Handler interface. But there is no context for the http handlers in the standard library.
But Go, besides the main standard library, there are Go authors and a
separate group of packages that are developed outside the main Go code, and do not have a hard promise of backward compatibility, as in the standard library. These are all packages that lie in
golang / x / . Among them, for a long time there is a
net / context package that implements the very essence of contexts and about which we will talk today. At the time of this writing, this package is already used,
according to GoDoc , in 1560 packages.
In part, the lack of context in the standard library was the reason for the emergence of a whole zoo of web frameworks, each of which decided to transfer the request context in its own way.
Framework Zoo
Using the word "zoo" I exaggerate a little, because there is no particular problem. Standard net / http is good as a foundation, but as soon as you start writing some web service, you sooner or later come to the need to implement more advanced functionality - complex routing with grouping, advanced logging, authorization processing and access control, etc. , and this naturally turns into a kind of framework - many will probably need the same functionality and will like the same approach. But overall, there are usually a couple of the most popular frameworks, and they change from time to time,
competing in zero-memory allocations and speeds . Now, the palm of popularity, like
gin-gonic .
Each of the frameworks approached the problem of context transfer in its own way.
GorillaToolkit keeps a global map of values for each request, protecting requests to it by mutexes.
Goji and others keep a separate map for each request.
gocraft / web works with contexts through reflection. In the aforementioned Gin -
Context, in general, the key structure with which all handlers work. In
echo, the concept of Context generally hung all the processing functions of the request.
Each of these approaches can have its pros and cons, but they all have one minus - binding to the framework. As mentioned above, the context is a concept rather abstract and not limited to http requests. Let's break it down into examples.
Example
Let's start with a simple web service:
package main import ( "net/http" ) func handler(w http.ResponseWriter, r *http.Request) { w.Write([]byte("Hello World")) } func main() { http.HandleFunc("/", handler) http.ListenAndServe(":1234", nil) }
$ curl http://localhost:1234 Hello World
But here we want a primitive PIN-code authorization, we will create a simple middleware using http.HandlerFunc:
func needPin(h http.HandlerFunc) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { if r.FormValue("pin") != "9999" { http.Error(w, "wrong pin", http.StatusForbidden) return } h(w, r) } } ... http.HandleFunc("/", needPin(handler)) ...
$ curl http://localhost:1234 wrong pin $ curl http://localhost:1234?pin=9999 Hello World
Great, but now we want to read the pin from the SQL database, not the hard drive. Ok, for now let's add a global variable * sql.DB:
func needPin(h http.HandlerFunc) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { var pin string if err := db.QueryRow("SELECT pin FROM pins").Scan(&pin); err != nil { http.Error(w, "database error", http.StatusInternalServerError) return } ...
Complete code $ sqlite3 pins.db SQLite version 3.8.4.3 2014-04-03 16:53:12 Enter ".help" for usage hints. sqlite> CREATE TABLE pins(pin STRING); sqlite> INSERT INTO pins(pin) VALUES ("9999"); sqlite> ^D
package main import ( "database/sql" _ "github.com/mattn/go-sqlite3" "net/http" ) var ( db *sql.DB err error ) func handler(w http.ResponseWriter, r *http.Request) { w.Write([]byte("Hello World")) } func needPin(h http.HandlerFunc) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { var pin string if err := db.QueryRow("SELECT pin FROM pins").Scan(&pin); err != nil { http.Error(w, "database error", http.StatusInternalServerError) return } if r.FormValue("pin") != pin { http.Error(w, "wrong pin", http.StatusForbidden) return } h(w, r) } } func main() { if db, err = sql.Open("sqlite3", "./pins.db"); err != nil { panic(err) } defer db.Close() http.HandleFunc("/", needPin(handler)) http.ListenAndServe(":1234", nil) }
And then the list of our requirements and desires begins to grow:
- Can every request with the wrong pin-code be logged to syslog?
- Can I still check and record the IP address?
- and it is possible to do sharding for different IP and run in different bases?
- Can i ...?
“Of course you can,” you think, and you write your own middleware for each new task, starting to assemble them into chains. There are even separate packages for this, for example
Alice . But how to pass the authorization error from authMiddleware to logMiddleware? And if you still need to run in
Vault for temporary passwords, first extracting the token from the session? And how to get rid of global variables and pass the connection object to the base to the handler?
This is where the concept of context comes to the rescue. Each middleware-function sets its values in the context variable, and passes the request and the context further. In this context, you are free to thrust anything at all — authorization status, user name, access permissions, information retrieved from the initial request headers, a pointer to a connection to the database, a unique ID for further tracking, and so on.
Usually context.Context is passed as a function parameter (both middleware and any function that works with a request). In principle, you can save the Context as a structure field, but this must be done carefully, since the Context must be bound to the request — created for each request and deleted after the processing of the request is completed.
But
net / context is not only about storing values, it is a unified approach to managing timeouts and canceling a request. Let's take a closer look.
net / context inside
The net / context API is a bit unusual, so be prepared for surprise at the beginning.
It is important to understand here, as usual, Go creates complex pipelines for processing the request - usually it is a gorutin that accepts a single request that generates one or more gorutins that processes this query or a stream of queries, which in turn return the result to the top. This can be as simple synchronous calls of functions, possibly separated into separate packages, as well as entire cascades of new Gorutin and channels transmitting channels. The main thing is that there are clear limits of responsibility of each middleware, each package, and each of them wants to know something about the request, and wants to do something about it.
Therefore, the first and main principle of net / context operation is that
contexts are nested and have a wooden structure. Actually, the
main functions of the package net / context and deal with those. what creates new variations of context from an already existing one, gives rise to a new “subcontext”:
func Background() Context func TODO() Context func WithCancel(parent Context) (ctx Context, cancel CancelFunc) func WithDeadline(parent Context, deadline time.Time) (Context, CancelFunc) func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc) func WithValue(parent Context, key interface{}, val interface{}) Context
The second important point is that context.Context is an interface, and the net / context package provides only a few variations, but you are free to create your contexts of any complexity.
type Context interface { Deadline() (deadline time.Time, ok bool) Done() <-chan struct{} Err() error Value(key interface{}) interface{} }
Let's take a look at the main types of existing contexts:
- context.Background () is an empty context, it has no values, no timeout or the ability to cancel; as a rule, Background () is used in the function that first receives an incoming request and is the basis for all subsequent derived queries.
Here is an example from camilstore:
client := oauth2.NewClient(context.Background(), google.ComputeTokenSource(""))
- context.TODO () is a special context, also empty, but used when it is unclear what context to use, or if the function has not yet been refactored to accept the context. The TODO name was chosen specifically so that static code analyzers could easily find this case. At the moment, however, the linter, which analyzes contexts is not yet in open source, but it is in Google and will be open in the future .
- context.WithCancel (parent Context) (ctx Context, cancel CancelFunc) - returns a copy of the context of the parent with the new channel Done and the function CancelFunc, which initiates the closing of this channel.
Let's see how it works:
func handler(w http.ResponseWriter, r *http.Request) {
This example is deliberately simplified, and we will look at how to use the context for timeouts just below, but the idea should be clear.
Forced Cancel can be useful, for example, in the case of a simultaneous request to different replicas of a service — after the response of the fastest replica, requests to the others can be safely canceled so that resources are not wasted. - context.WithTimeout (parent Context, timeout time.Duration) (Context, CancelFunc) and context.WithDeadline (parent Context, deadline time.Time) (Context, CancelFunc) - similar to WithCancel (), but they close Done after a timeout or achievement deadline Actually, WithTimeout is WithDeadline with the time.Now (). Add (timeout) parameter.
If you understand how WithCancel works, then there should be no problems with these two functions. The example above will be simplified by a couple of lines, and you can do something like this:
timeout, err := time.ParseDuration(req.FormValue("timeout")) if err == nil { ctx, cancel = context.WithTimeout(context.Background(), timeout) }
to indicate how long to wait for a long query. - context.WithValue (parent Context, key interface {}, val interface {}) Context - serves for the above task of storing data or resources associated with the context
A little more about context.WithValue
Unlike the frameworks mentioned above, net / context does not use maps or any similar data structures due to its tree-like nested architecture. One context carries one value plus a parent context. The new meaning is already a new context. Both the key and the value itself can be of any type.
The standard mechanism for using this is that the key must be a non-exported type to avoid collisions with other packages / APIs that can work with the context. For example:
package userIP
The very context with the value looks like this (https://github.com/golang/net/blob/master/context/context.go#L433):
type valueCtx struct { Context key, val interface{} }
As you can see, for any nesting of the context, Value () will go up the context tree until it reaches the desired value of the desired type. Well, or returns nil, since Value () is defined for all contexts (Context, as you remember, is an interface, and, therefore, is also defined for Background-context too).
The complete code of the example package working with context values might be something like this:
What and how you will put in context depends on the specific task. This can be both a simple user ID, a complex structure with a lot of information inside, and an object like sql.DB for working with a database.
Buns
Implementing a context as a universal interface allows you to be friends with code that uses contexts from other frameworks with code that uses net / context.
Here is an example of a context using gorilla / context:
blog.golang.org/context/gorilla/gorilla.goAnd here is an example of working with canceling a request in another framework, tomb:
blog.golang.org/context/tomb/tomb.goOr the
net / trace package, as an example of using context to trace the life of a Dapper-style query. The parent context generates context.WithValue (ctx, trace) and all subsequent calls and contexts that will be generated during the processing of the request will contain the ID of the trace, and already the net / trace code contains the necessary handlers that provide information about the traces on the web page paths
/ debug / requests and
/ debug / events .
Obviously, the greatest benefit will be if all the code that communicates with external resources or generates new gorutiny, will use net / context. For example, the Google code base on Go, which already has about 10 + million lines of code, uses context.Context everywhere. The new RPC framework
gRPC on Protobuf3, when it generates code for Go, also transmits context.Context everywhere.
Future plans
Here is
an active discussion of future plans for net / context, and it is likely that the context will appear in the standard library in Go 1.7. Perhaps with minor changes, perhaps without, but, in any case, there is interest and desire, so it’s worth keeping your finger on the pulse.
The standard library, of course, will be backward compatible, and there will not be things like separation into http and ctxhttp, so as not to fragment the code base (although the
ctxhttp package now exists as an experiment). Perhaps the Context field will be added to http.Request, and it may come to some other variant.
The net word from net / context is likely to disappear.
Links
If you want to sort out and see more examples, I highly recommend the following links:
Good article on contexts on the Go blog -
blog.golang.org/contextReport on GothamGo 2014 about contexts -
vimeo.com/115309491Another article on http.Handler and net / context -
joeshaw.org/net-context-and-http-handlerDetailed slides of a good report on the topic net / context -
go-talks.appspot.com/github.com/guregu/slides/kami/kami.slide#1Article on advanced pipelines and cancellations in Go -
blog.golang.org/pipelinesAn overview of context implementations in various frameworks -
www.nicolasmerouze.com/share-values-between-middlewares-context-golangSource code net / context -
github.com/golang/net/tree/master/context