Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Passing context to gorilla mux - go idioms

Tags:

go

gorilla

I'm reasonably new to golang and am trying to do work out the best way to do this idiomatically.

I have an array of routes I am statically defining and passing to gorilla/mux. I am wrapping each handler function with something to time the request and handle panics (mainly so I could understand how the wrapping worked).

I want them each to be able to have access to a 'context' - a struct that's going to be one-per-http-server, which might have things like database handles, config etc. What I don't want to do is use a static global variable.

The way I'm currently doing it I can give the wrappers access to the context structure, but I can't see how to get this into the actual handler, as it wants that to be an http.HandlerFunc. I thought what I could do is convert http.HandlerFunc into a type of my own that was a receiver for Context (and do similarly for the wrappers, but (after much playing about) I couldn't then get Handler() to accept this.

I can't help but think I'm missing something obvious here. Code below.

package main

import (
    "fmt"
    "github.com/gorilla/mux"
    "html"
    "log"
    "net/http"
    "time"
)

type Route struct {
    Name        string
    Method      string
    Pattern     string
    HandlerFunc http.HandlerFunc
}

type Context struct {
    route *Route
    // imagine other stuff here, like database handles, config etc.
}

type Routes []Route

var routes = Routes{
    Route{
        "Index",
        "GET",
        "/",
        index,
    },
    // imagine lots more routes here
}

func wrapLogger(inner http.Handler, context *Context) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        start := time.Now()

        inner.ServeHTTP(w, r)

        log.Printf(
            "%s\t%s\t%s\t%s",
            r.Method,
            r.RequestURI,
            context.route.Name,
            time.Since(start),
        )
    })
}

func wrapPanic(inner http.Handler, context *Context) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        defer func() {
            if err := recover(); err != nil {
                log.Printf("panic caught: %+v", err)
                http.Error(w, http.StatusText(500), 500)
            }
        }()

        inner.ServeHTTP(w, r)
    })
}

func newRouter() *mux.Router {

    router := mux.NewRouter().StrictSlash(true)
    for _, route := range routes {
        // the context object is created here
        context := Context {
            &route,
            // imagine more stuff here
        }
        router.
            Methods(route.Method).
            Path(route.Pattern).
            Name(route.Name).
            Handler(wrapLogger(wrapPanic(route.HandlerFunc, &context), &context))
    }

    return router
}

func index(w http.ResponseWriter, r *http.Request) {
    // I want this function to be able to have access to 'context'
    fmt.Fprintf(w, "Hello, %q", html.EscapeString(r.URL.Path))
}

func main() {
    fmt.Print("Starting\n");
    router := newRouter()
    log.Fatal(http.ListenAndServe("127.0.0.1:8080", router))
}

Here's a way to do it, but it seems pretty horrible. I can't help but think there must be some better way to do it - perhaps to subclass (?) http.Handler.

package main

import (
    "fmt"
    "github.com/gorilla/mux"
    "html"
    "log"
    "net/http"
    "time"
)

type Route struct {
    Name        string
    Method      string
    Pattern     string
    HandlerFunc ContextHandlerFunc
}

type Context struct {
    route  *Route
    secret string
}

type ContextHandlerFunc func(c *Context, w http.ResponseWriter, r *http.Request)

type Routes []Route

var routes = Routes{
    Route{
        "Index",
        "GET",
        "/",
        index,
    },
}

func wrapLogger(inner ContextHandlerFunc) ContextHandlerFunc {
    return func(c *Context, w http.ResponseWriter, r *http.Request) {
        start := time.Now()

        inner(c, w, r)

        log.Printf(
            "%s\t%s\t%s\t%s",
            r.Method,
            r.RequestURI,
            c.route.Name,
            time.Since(start),
        )
    }
}

func wrapPanic(inner ContextHandlerFunc) ContextHandlerFunc {
    return func(c *Context, w http.ResponseWriter, r *http.Request) {
        defer func() {
            if err := recover(); err != nil {
                log.Printf("panic caught: %+v", err)
                http.Error(w, http.StatusText(500), 500)
            }
        }()

        inner(c, w, r)
    }
}

func newRouter() *mux.Router {

    router := mux.NewRouter().StrictSlash(true)
    for _, route := range routes {
        context := Context{
            &route,
            "test",
        }
        router.Methods(route.Method).
            Path(route.Pattern).
            Name(route.Name).
            HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
            wrapLogger(wrapPanic(route.HandlerFunc))(&context, w, r)
        })
    }

    return router
}

func index(c *Context, w http.ResponseWriter, r *http.Request) {
    fmt.Fprintf(w, "Hello, %q secret is %s\n", html.EscapeString(r.URL.Path), c.secret)
}

func main() {
    fmt.Print("Starting\n")
    router := newRouter()
    log.Fatal(http.ListenAndServe("127.0.0.1:8080", router))
}
like image 562
abligh Avatar asked Jun 26 '15 12:06

abligh


1 Answers

I am learning Go and currently in the middle of a nearly identical problem, and this is how I've dealt with it:


First, I think you missed an important detail: There are no global variables in Go. The widest scope you can have for a variable is package scope. The only true globals in Go are predeclared identifiers like true and false (and you can't change these or make your own).

So, it's perfectly fine to set a variable scoped to package main to hold context for your program. Coming from a C/C++ background this took me a little time to get used to. Since the variables are package scoped, they do not suffer from the problems of global variables. If something in another package needs such a variable, you will have to pass it explicitly.

Don't be afraid to use package variables when it makes sense. This can help you reduce complexity in your program, and in a lot of cases make your custom handlers much simpler (where calling http.HandlerFunc() and passing a closure will suffice).

Such a simple handler might look like this:

func simpleHandler(c Context, next http.Handler) http.Handler {
  return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
    // FIXME Do something with our context
    next.ServeHTTP(w, r)
  })
}

and be used by:

r = mux.NewRouter()
http.Handle("/", simpleHandler(c, r))

If your needs are more complex, you may need to implement your own http.Handler. Remember that an http.Handler is just an interface which implements ServeHTTP(w http.ResponseWriter, r *http.Request).

This is untested but should get you about 95% of the way there:

package main

import (
    "net/http"
)

type complicatedHandler struct {
    h    http.Handler
    opts ComplicatedOptions
}

type ComplicatedOptions struct {
    // FIXME All of the variables you want to set for this handler
}

func (m complicatedHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    // FIXME Do stuff before serving page

    // Call the next handler
    m.h.ServeHTTP(w, r)

    // FIXME Do stuff after serving page
}

func ComplicatedHandler(o ComplicatedOptions) func(http.Handler) http.Handler {
    return func(h http.Handler) http.Handler {
        return complicatedHandler{h, o}
    }
}

To use it:

r := mux.NewRouter()
// FIXME: Add routes to the mux

opts := ComplicatedOptions{/* FIXME */}
myHandler := ComplicatedHandler(opts)

http.Handle("/", myHandler(r))

For a more developed handler example see basicAuth in goji/httpauth, from which this example was shamelessly ripped off.


Some further reading:

  • A Recap of Request Handling
  • Making and Using HTTP Middleware
  • justinas/alice (for chaining lots of handlers)
like image 117
Michael Hampton Avatar answered Oct 05 '22 17:10

Michael Hampton