One thing I try to avoid when I can is using the context.WithValue(). It’s a black box map of map[interface{}]interface{}. This is obviously flexible because anything can be stored here but there are pitfalls. The documentation states you need to create your own types for keys: The provided key must be comparable and should not be of type string or any other built-in type to avoid collisions between packages using context. Users of WithValue should define their own types for keys. You need to type assert anything that is stored as a value since it’s just an empty interface. That means some kind of getter to retrieve values to ensure they’re the correct type. Your functions are now also kind of a black box. Unless the caller reads through the entire function or you document exactly what is needed to be passed in the context values, you have no way of knowing what the function actually needs. Imagine if a function looked like this func callme(args map[interface{}]interface{}). That would be impossible to know what parameters are needed.

I think a more elegant solution is to just create a custom handler that we can simply pass a parameter with the data we want. There are a few different ways to do this, but one way I really like is creating a custom type that satiesfies the http.Handler interface. We can not only pass whatever parameters we want, we can also handle errors in a centralized location. The final product below will look more complicated than just using val := ctx.Value(key). However I argue that A) it only looks big because it’s one file for brevity sake, B) we dealt with more than just passing a value, and C) when you have many routes and many parameters it will make everything more readable. Let’s look at how this would work.

Types

First we need a function definition and a struct that will contain the data we need.

package main

import (
	"net/http"
)

type DataStore interface {
	GetUser(ctx context.Context, id string) *user
}

type user struct {
	id   string
	name string
	role string
}

type handlerFunc func(http.ResponseWriter, *http.Request, *user) error

type customHandler struct {
	handler handlerFunc
	ds      DataStore
}

customHandler conatains both a handler and a datasource. To keep things simple, we have an in memory datasource that holds a map of users.

Error Handling

Now we need the types and methods for our error handling

type customError struct {
	status  int
	error string
}

type clientError interface {
	Error() string
	Resp() []byte
	Status() int
}

func (c *customError) Error() string {
	return c.error
}

func (c *customError) Resp() []byte {
	return []byte(fmt.Sprintf(`{"error": "%s"}`, c.error))
}

func (c *customError) Status() int {
	return c.status
}

func NewCustomError(err error, status int) error {
	return &customError{
		status:  status,
		error: err.Error(),
	}
}

What we have here is a custom struct with the very original name customError. This struct both satisfies the error interface and the clientError interface. Since it satisfies the error interface we can return it as an error as normal. But we can also type assert that an error is or isn’t a customError. If we don’t have a customError we just return a 500 status with an internal server error message. However, if it’s a 400 type error we can call NewCustomError and fill in the details.

Handler Interface

Now that we have our types, let’s make customHandler satisfy the http.Handler interface. We will assume that the user ID is passed as a query param named id.

func (c customHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	id := r.URL.Query().Get("id")
	
	user := c.ds.GetUser(r.Context(), id)
	
	err := c.handler(w, r, user)
	if err == nil {
		return
	}
	
	ce, ok := err.(clientError)
	if !ok {
		w.WriteHeader(http.StatusInternalServerError)
		w.Write([]byte("internal server error"))
	}
	
	w.WriteHeader(ce.Status())
	w.Write(ce.Resp())
}

Now that we’ve satisfied the http.Handler interface, we can call our customHandler in a router.

Routes

For ease of use, lets create some boilerplate around creating routes.

type route struct {
	Name    string
	Method  string
	Path    string
	Handler customHandler
}

func getRoutes(ds DataStore) []route {
	return []route{
		{
			Name:    "getID",
			Method:  http.MethodGet,
			Path:    "/getID",
			Handler: customHandler{printId, ds},
		},
	}
}

func printId(w http.ResponseWriter, r *http.Request, u *user) error {
    if u == nil {
		return NewCustomError(fmt.Errorf("user not found"), 404)
	}

	w.Write([]byte(u.id))

	return nil
}

This is a simple example, so we just have a method that returns a slice of routes. A route holds all the information for a specific route: name, method, path, and handler. The reason for building routes this way is so your main function doesn’t have many lines of routes with struct definitions.

Main

Finally in our main we can call getRoutes and add those routes to our router.

func main() {
	r := http.NewServeMux()
	ds := newMemDS()

	for _, v := range getRoutes(ds) {
		r.Handle(v.Path, v.Handler)
	}

	log.Fatal(http.ListenAndServe(":8080", r))
}

Putting It All Together

Here’s the final product (including the datasource). This should be ready for you to copy paste and try out. Once you run the server with go run, you can try out the requests with curl "localhost:8080/getID?id=123" which should return 123. Changing the ID should return user not found.

package main

import (
	"fmt"
	"log"
	"net/http"
)

type DataStore interface {
    GetUser(id string) *user
}
type user struct {
    id   string
    name string
    role string
}

type memoryStore struct {
	users map[string]*user
}

func (m *memoryStore) GetUser(id string) *user {
	return m.users[id]
}

func newMemDS() *memoryStore {
	return &memoryStore{
		users: map[string]*user{
			"123": &user{
				id:   "123",
				name: "test user",
				role: "admin",
			},
		},
	}
}

type handlerFunc func(http.ResponseWriter, *http.Request, *user) error

type customHandler struct {
	handler handlerFunc
	ds      DataStore
}

type customError struct {
	status int
	error  string
}

type clientError interface {
	Error() string
	Resp() []byte
	Status() int
}

func (c *customError) Error() string {
	return c.error
}

func (c *customError) Resp() []byte {
	return []byte(fmt.Sprintf(`{"error": "%s"}`, c.error))
}

func (c *customError) Status() int {
	return c.status
}

func NewCustomError(err error, status int) error {
	return &customError{
		status: status,
		error:  err.Error(),
	}
}

func (c customHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	id := r.URL.Query().Get("id")

	user := c.ds.GetUser(id)

	err := c.handler(w, r, user)
	if err == nil {
		return
	}

	ce, ok := err.(clientError)
	if !ok {
		w.WriteHeader(http.StatusInternalServerError)
		w.Write([]byte("internal server error"))
	}

	w.WriteHeader(ce.Status())
	w.Write(ce.Resp())
}

type route struct {
	Name    string
	Method  string
	Path    string
	Handler customHandler
}

func getRoutes(ds DataStore) []route {
	return []route{
		{
			Name:    "getID",
			Method:  http.MethodGet,
			Path:    "/getID",
			Handler: customHandler{printId, ds},
		},
	}
}

func printId(w http.ResponseWriter, r *http.Request, u *user) error {
    if u == nil {
		return NewCustomError(fmt.Errorf("user not found"), 404)
	}

	w.Write([]byte(u.id))

	return nil
}

func main() {
	r := http.NewServeMux()
	ds := newMemDS()

	for _, v := range getRoutes(ds) {
		r.Handle(v.Path, v.Handler)
	}

	log.Fatal(http.ListenAndServe(":8080", r))
}