We saw in the last part that with a simple function like func (http.Handler) http.Handler
we could create middlewares and share pieces of code between our routes and even our different apps. We took two examples: logging and panic recovery. But most middlewares will be more complex than that. And for some cases we will need to pass values to the next middlewares.
For example with an authentication middleware that would check if a user exists in the database and retrieve it. We need this information down the middleware stack and in the handler processing the request. This article will show you the different value sharing solutions also known as contexts.
This is the third article in the five-part series "Build Your Own Web Framework in Go":
- Part 1: Introduction
- Part 2: Middlewares in Go: Best practices and examples
- Part 3: Share values between middlewares
- Part 4: Guide to 3rd Party Routers
- Part 5: How to implement JSON-API standard with MongoDB
- Bonus: File Upload REST API with Go and Amazon S3
How developers are handling it
In Ruby, most developers and frameworks are using Rack. Each middleware gets an env
hash map containing request information and in which you can set anything.
In Node.js, there is Connect.js. Each middleware has two objects, one for the request and one for the response. Objects in Javascript can act as hash maps so developers are using the request object to store anything they want.
For a statically-typed language, it's not that different. I used Netty in Java a few times and it uses maps too. Each handler will have access to a context object containing an attribute map.
But how Go packages/frameworks are handling contexts? Here's a few examples.
Gorilla
gorilla/context defines a map of requests each containing a map[interface{}]interface{}]
used to store information during the request processing. It uses a mutex to keep it threadsafe. Here's how it looks like:
var (
mutex sync.RWMutex
data = make(map[*http.Request]map[interface{}]interface{})
)
And here's how to get and set a value:
func myHandler(w http.ResponseWriter, r *http.Request) {
context.Set(r, "foo", "bar")
}
func myOtherHandler(w http.ResponseWriter, r *http.Request) {
val := context.Get(r, "foo").(string)
}
The map is not cleared automatically. Each request will add an entry and the map will grow indefinitely. So Gorilla also has a handler to clear the map from the current request we must use before the end of the processing (it is automatic when used in conjonction to gorilla/mux).
Pros:
- Handlers and middlewares can stay as they are, it's just a call inside them.
Cons:
- I have made some micro-benchmarks and it's the slowest solution. The performance hit is minor in a real-world application, though.
- We are using a mutex here. Some will say it's not idiomatic, some say the opposite. The Go documentation is not against mutexes.
- Since we are using
interface{}
we must assert the value is of type x.
Goji
Goji uses a struct containing URL params and has a map[string]interface{}
named Env
. it is passed to each middleware without the use of a mutex.
func myMiddleware(c *web.C, h http.Handler) http.Handler {
fn := func (w http.ResponseWriter, r *http.Request) {
c.Env["name"] = "world"
h.ServeHTTP(w, r)
}
return http.HandlerFunc(fn)
}
func hello(c web.C, w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Hello, %s!", c.Env["name"].(string))
}
Pros:
- No mutex.
Cons:
- Not compatible with our middleware system. It's not completely different though.
- Type assertions needed.
gocraft/web
gocraft/web takes any struct we define in our code we want and uses it as a context. No map, no type assertion. It's great for performance but it's the least compatible way to do it. You'll have to create a new context struct for each app and you'll not be able to reuse your middlewares.
Pros:
- No mutex.
- No type assertions.
Cons:
- Not compatible with our middleware system.
- Code wrote for this framework can't be reuse without lots of changes.
go.net/context
This is the package used internally at Google to handle contexts. You can read their blog post about it. It is a really nice solution and they really want to unify all the contexts packages available. The problem is it doesn't play nice with our middleware system.
Global view
Here's a table representing the different solutions:
package | mutex | map | struct |
---|---|---|---|
go.net/context | n | y* | n |
goji | n | y | n |
gin | n | y | n |
martini | n | y | n |
gorilla | y | y | n |
tigertonic | y | n | y |
gocraft/web | n | n | y |
* go.net/context does not use a map but it's very similar.
You see that most contexts are maps or something similar. All the solutions have advantages and problems. Some might say type assertion is to be avoided as much as possible, but the code overhead in using structs for contexts is similar (though performances are better without type assertion).
Contexts with maps and mutexes are 10% less performant than those without mutexes. struct contexts are the fastest. But in real-life scenarios with, let's say, 10ms to process each request, it is less than 1%, not a big change. You might opt-in for performances if it really matters to you.
I think maps are the most flexible solution. The two types of context I've shown in the comparison using maps are gorilla/context and Goji. gorilla/context is a solution given by one of the Go creators. And Goji contexts are the easiest to implement when starting from scratch. For this article, we will use gorilla/context.
Depending on if you want to share your middleware with the rest of the world, you may adopt a more standard interface. For example, nosurf uses its own context based on the same concept as gorilla/context and has a standard middleware interface. You can use it in almost any project built with any framework without issues. That's why the gorilla/context system is better, it's easier to reuse middlewares.
Integrate contexts into our own framework
Since we're using gorilla/context, we almost don't need to change anything to our existing code. When presenting gorilla/context, I have said that the map of requests storing contexts is not cleared automatically so for each new request there will be a new entry in the map and it will grow endlessly. the package has a ClearHandler
handler to fix the issue.
func main() {
commonHandlers := alice.New(context.ClearHandler, loggingHandler, recoverHandler)
http.Handle("/about", commonHandlers.ThenFunc(aboutHandler))
http.Handle("/", commonHandlers.ThenFunc(indexHandler))
http.ListenAndServe(":8080", nil)
}
Now we can add a middleware storing something we need in our main handler. Let's create an authentication middleware. This middleware will get the Authorization
header containing the token and will search for a user. If the authentication fails, the middleware will not execute the next midlewares and will return an error. If it succeeds, it will store the user in the context and execute the next middleware.
func authHandler(next http.Handler) http.Handler {
fn := func(w http.ResponseWriter, r *http.Request) {
authToken := r.Header().Get("Authorization")
user, err := getUser(authToken)
if err != nil {
http.Error(w, http.StatusText(401), 401)
return
}
context.Set(r, "user", user)
next.ServeHTTP(w, r)
}
return http.HandlerFunc(fn)
}
func adminHandler(w http.ResponseWriter, r *http.Requests) {
user := context.Get(r, "user")
json.NewEncoder(w).Encode(user)
}
func main() {
commonHandlers := alice.New(context.ClearHandler, loggingHandler, recoverHandler)
http.Handle("/admin", commonHandlers.Append(authHandler).ThenFunc(adminHandler))
http.Handle("/about", commonHandlers.ThenFunc(aboutHandler))
http.Handle("/", commonHandlers.ThenFunc(indexHandler))
http.ListenAndServe(":8080", nil)
}
getUser
is the function finding the user. Let's say the user
is a map[string]interface{}
to simplify our example. In our admin handler we get the user we stored in the authentication middleware, we encode it and write it in the response writer.
Application-level values
There's still a problem with this approach. getUser
will certainly want an access to a database. Our context is bound to a request, storing a DB connection reference in the context of each request isn't great. It would be better to store this reference somewhere and all the requests will access it. We could use global/package level variables as such:
var dbConn *sql.DB
func main() {
dbConn := sql.Open("postgres", "...")
}
We could access dbConn
in the entire application which would solve the problem. *sql.DB
is a pool so it's safe for concurrent use. But in my experience global variables is really bad for maintenance. You don't control what can modify them and it is difficult to track their state. Refactoring can become a real pain, even with perfect testing.
Another solution would be to have a struct with all the values we need to use and create handlers and middlewares as methods of this struct. To solve our problem, it would be a struct with a db
field containg our connection pool. This struct would have methods for our authHandler
and adminHandler
. The code from above will only sligthly change to use db
for getUser
function.
type appContext struct {
db *sql.DB
}
func (c *appContext) authHandler(next http.Handler) http.Handler {
fn := func(w http.ResponseWriter, r *http.Request) {
authToken := r.Header.Get("Authorization")
user, err := getUser(c.db, authToken)
if err != nil {
http.Error(w, http.StatusText(401), 401)
return
}
context.Set(r, "user", user)
next.ServeHTTP(w, r)
}
return http.HandlerFunc(fn)
}
func (c *appContext) adminHandler(w http.ResponseWriter, r *http.Request) {
user := context.Get(r, "user")
// Maybe other operations on the database
json.NewEncoder(w).Encode(user)
}
func main() {
db := sql.Open("postgres", "...")
appC := appContext{db}
commonHandlers := alice.New(context.ClearHandler, loggingHandler, recoverHandler)
http.Handle("/admin", commonHandlers.Append(appC.authHandler).ThenFunc(appC.adminHandler))
http.Handle("/about", commonHandlers.ThenFunc(aboutHandler))
http.Handle("/", commonHandlers.ThenFunc(indexHandler))
http.ListenAndServe(":8080", nil)
}
It is integrated very nicely with our middleware system and the code has not really changed. This approach uses a struct, like gocraft/web, but only for application-level values and is not tied to any specific code making it reusable across apps and even other frameworks.
We could alternatively make
getUser
a method ofappContext
to be cleaner. Or wrap*sql.DB
in a custom struct and addgetUser
as a method of this custom struct so we could call it simply withc.db.getUser(token)
.
Wrap up
We now have a way to pass our authenticated user and other stored values by middlewares into the main handler. It doesn't require much change to our application and we can mix both standard middlewares and middleware with contexts without much more code. The only missing part is now the router, subject of the next part. But with all we made until now, integrate a router to our framework will be very easy.
Resources
There have been a number of articles on this subject that I recommend you to read: