In the last post I covered a way to pass data to http handlers without using context.WithValue()
. I saw another interesting way to do a type of dependency injection in a package from Jeremy called Mixer. If you own the server implementation and don’t want to import a 3rd party package you can do something similar. This doesn’t rely on generics so it is backwards compatible with older versions of Go.
First create a struct to hold our data we would like to pass to the handlers including the same DataStore implementation from the last post:
type DataStore interface {
GetUser(id string) *user
}
type memoryStore struct {
users map[string]*user
}
type user struct {
ID string `json:"id,omitempty"`
Name string `json:"name,omitempty"`
Role string `json:"role,omitempty"`
}
type AppContext struct {
r *http.Request
w http.ResponseWriter
user *user
err error
datastore DataStore
}
The one downside to this approach is if you don’t wrap your handlers with a function, the signature of your function ends up looking like this func (a *AppContext)
. Luckily we can wrap this and return an http.HandlerFunc
.
Let’s define our app function:
type appFunc func(a *AppContext)
Now that we have our function layout, here’s where the first part of the magic happens. We take an appFunc
and do some work to create our app context. Then we will return an http.HandleFunc
which the standard library knows how to handle (no pun intended).
func (s *Server) ToHandler(fn appFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
a := &AppContext{
w: w,
r: r,
datastore: s.datastore,
}
for _, v := range s.before {
v(a)
}
fn(a)
for _, v := range s.after {
v(a)
}
}
}
We create an app context and set the response writer and request to the HandleFunc response writer and request. We also add the datastore to the AppContext so we can query the datastore later.
The before and after ranges are there for any middleware we want to set up. There lies the second (minor) downside with this approach, the middleware won’t accept a standard http.Handler like most of us are used to. We just pass the app context to the before and after middleware functions. Let’s create the struct to hold the middlewares and our DataStore:
type Server struct {
before []appFunc
after []appFunc
datastore DataStore
}
Similarly to what we did in the last post, let’s create some helpers to build our routes:
type route struct {
handler func(http.ResponseWriter, *http.Request, *user) error
method string
path string
}
func getRoutes() []route {
return []route{
{
path: "/getUser",
handler: getUser,
method: http.MethodGet,
},
}
}
Remember earlier when I said this was the first part of the magic? Well here’s the second part. If all we did was set up the function above, our handlers would still look like the appFunc
type. This function adds our routes to our muxer. It takes the handler in our route and maps the AppContext
fields to the handlers arguments. Now we can make our typical looking handlers with a responsewriter, request, and whatever other arguments:
func (s *Server) buildRouter(routes []route, m *http.ServeMux) *http.ServeMux {
for _, v := range routes {
m.Handle(v.path, s.ToHandler(func(a *AppContext) {
a.err = v.handler(a.w, a.r, a.user)
}))
}
return m
}
Now we can write our handler which just takes an http.ResponseWriter
, *http.Request
, and a *user
:
func getUser(w http.ResponseWriter, r *http.Request, u *user) error {
if u == nil {
return fmt.Errorf("user found not")
}
w.Header().Add("Content-Type", "application/json")
if err := json.NewEncoder(w).Encode(u); err != nil {
return err
}
return nil
}
All we need to do now is wire it up in a main()
and add our datastore implementation. This doesn’t have custom error handling like the last post, but that could easily be added in the error handling middleware. As before, you should be able to copy paste this and run it:
package main
import (
"encoding/json"
"fmt"
"log"
"net/http"
)
type DataStore interface {
GetUser(id string) *user
}
type memoryStore struct {
users map[string]*user
}
type user struct {
ID string `json:"id,omitempty"`
Name string `json:"name,omitempty"`
Role string `json:"role,omitempty"`
}
// Server holds any before and after middleware. It also holds our
// datastore interface.
type Server struct {
before []appFunc
after []appFunc
datastore DataStore
}
type appFunc func(a *AppContext)
// AppContext is the context passed around to our handlers
type AppContext struct {
r *http.Request
w http.ResponseWriter
user *user
err error
datastore DataStore
}
// route holds the information for a route in our muxer
type route struct {
handler func(http.ResponseWriter, *http.Request, *user) error
method string
path string
}
// newMemDS creates a new in memory user store
func newMemDS() *memoryStore {
return &memoryStore{
users: map[string]*user{
"123": {
ID: "123",
Name: "test user",
Role: "admin",
},
},
}
}
// GetUser returns the user associated with the supplied ID
func (m *memoryStore) GetUser(id string) *user {
return m.users[id]
}
// getUser is our http handler for getting user information
func getUser(w http.ResponseWriter, r *http.Request, u *user) error {
if u == nil {
return fmt.Errorf("user found not")
}
w.Header().Add("Content-Type", "application/json")
if err := json.NewEncoder(w).Encode(u); err != nil {
return err
}
return nil
}
// injectUser queries the datastore for a user and if found
// adds it to our appContext.
func injectUser(a *AppContext) {
id := a.r.URL.Query().Get("id")
a.user = a.datastore.GetUser(id)
}
// handleErrors is just middleware to print out any errors
func handleErrors(a *AppContext) {
if a.err != nil {
a.w.WriteHeader(500)
a.w.Write([]byte(a.err.Error()))
}
}
// ToHandler takes an appFunc and returns an http.HandlerFunc. It maps
// the AppContext writer, request to the http.HandlerFunc incoming writer and request.
// It also runs any before/after middleware set on the server.
func (s *Server) ToHandler(fn appFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
a := &AppContext{
w: w,
r: r,
datastore: s.datastore,
}
for _, v := range s.before {
v(a)
}
fn(a)
for _, v := range s.after {
v(a)
}
}
}
// get routes returns a slice of routes
func getRoutes() []route {
return []route{
{
path: "/getUser",
handler: getUser,
method: http.MethodGet,
},
}
}
// buildRouter takes an array of routes and a muxer and attaches the routes to the
// muxer while mapping the writer, request, and user to the handler. It also sets
// the error from the function to the app context error field.
func (s *Server) buildRouter(routes []route, m *http.ServeMux) *http.ServeMux {
for _, v := range routes {
m.Handle(v.path, s.ToHandler(func(a *AppContext) {
a.err = v.handler(a.w, a.r, a.user)
}))
}
return m
}
func main() {
mux := http.NewServeMux()
m := newMemDS()
s := Server{
before: []appFunc{injectUser},
after: []appFunc{handleErrors},
datastore: m,
}
routes := getRoutes()
router := s.buildRouter(routes, mux)
log.Fatal(http.ListenAndServe(":8888", router))
}