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))
}