Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Passing Context to Interface Methods

Tags:

go

Somewhat inspired by this article last week, I'm toying with refactoring an application I have to more explicitly pass context (DB pools, session stores, etc) to my handlers.

However, one issue I'm having is that without a global templates map, the ServeHTTP method on my custom handler type (as to satisfy http.Handler) can no longer access the map to render a template.

I need to either retain the global templates variable, or re-define my custom handler type as a struct.

Is there a better way to achieve this?

func.go

package main

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

    "html/template"

    "github.com/gorilla/sessions"
    "github.com/jmoiron/sqlx"
    "github.com/zenazn/goji/graceful"
    "github.com/zenazn/goji/web"
)

var templates map[string]*template.Template

type appContext struct {
    db    *sqlx.DB
    store *sessions.CookieStore
}

type appHandler func(w http.ResponseWriter, r *http.Request) (int, error)

func (ah appHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    // templates must be global for us to use it here
    status, err := ah(w, r)
    if err != nil {
        log.Printf("HTTP %d: %q", status, err)
        switch status {
        case http.StatusNotFound:
            // Would actually render a "http_404.tmpl" here...
            http.NotFound(w, r)
        case http.StatusInternalServerError:
            // Would actually render a "http_500.tmpl" here
            // (as above)
            http.Error(w, http.StatusText(status), status)
        default:
            // Would actually render a "http_error.tmpl" here
            // (as above)
            http.Error(w, http.StatusText(status), status)
        }
    }
}

func main() {
    // Both are 'nil' just for this example
    context := &appContext{db: nil, store: nil}

    r := web.New()
    r.Get("/", appHandler(context.IndexHandler))
    graceful.ListenAndServe(":8000", r)
}

func (app *appContext) IndexHandler(w http.ResponseWriter, r *http.Request) (int, error) {
    fmt.Fprintf(w, "db is %q and store is %q", app.db, app.store)
    return 200, nil
}

struct.go

package main

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

    "html/template"

    "github.com/gorilla/sessions"
    "github.com/jmoiron/sqlx"
    "github.com/zenazn/goji/graceful"
    "github.com/zenazn/goji/web"
)

type appContext struct {
    db        *sqlx.DB
    store     *sessions.CookieStore
    templates map[string]*template.Template
}

// We need to define our custom handler type as a struct
type appHandler struct {
    handler func(w http.ResponseWriter, r *http.Request) (int, error)
    c       *appContext
}

func (ah appHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    status, err := ah.handler(w, r)
    if err != nil {
        log.Printf("HTTP %d: %q", status, err)
        switch status {
        case http.StatusNotFound:
            // Would actually render a "http_404.tmpl" here...
            http.NotFound(w, r)
        case http.StatusInternalServerError:
            // Would actually render a "http_500.tmpl" here
            // (as above)
            http.Error(w, http.StatusText(status), status)
        default:
            // Would actually render a "http_error.tmpl" here
            // (as above)
            http.Error(w, http.StatusText(status), status)
        }
    }
}

func main() {
    // Both are 'nil' just for this example
    context := &appContext{db: nil, store: nil}

    r := web.New()
    // A little ugly, but it works.
    r.Get("/", appHandler{context.IndexHandler, context})
    graceful.ListenAndServe(":8000", r)
}

func (app *appContext) IndexHandler(w http.ResponseWriter, r *http.Request) (int, error) {
    fmt.Fprintf(w, "db is %q and store is %q", app.db, app.store)
    return 200, nil
}

Is there a cleaner way to pass the context instance to ServeHTTP?

Note that go build -gcflags=-m shows that neither option appears to be worse in teams of heap allocation: the &appContext literal escapes to the heap (as expected) in both cases, although my interpretation is that the struct-based option does pass a second pointer (to context) on each request—correct me if I'm wrong here as I'd love to get a better understanding of this.

I'm not wholly convinced that globals are bad in package main (i.e. not a lib) provided they are safe to use in that manner (read only/mutexes/a pool), but I do like clarity having to explicitly pass context provides.

like image 526
elithrar Avatar asked Jul 12 '14 15:07

elithrar


2 Answers

I would use a closure and do something like this:

func IndexHandler(a *appContext) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *httpRequest) {
        // ... do stuff
        fmt.Fprintf(w, "db is %q and store is %q\n", a.db, a.store)
    })
}

And just use the returned http.Handler.

You'll just have to make sure your appContext is goroutine-safe.

like image 176
seong Avatar answered Oct 27 '22 11:10

seong


After some discussion with a couple of helpful Gophers on #go-nuts, the method above is about "as good as it gets" from what I can discern.

  • The "con" with this method is that we pass a reference to our context struct twice: once as a pointer receiver in our method, and again as a struct member so ServeHTTP can access it as well.
  • The "pro" is that we can extend our struct type to accept a request context struct if we wanted to do so (like gocraft/web does).

Note that we can't define our handlers as methods on appHandler i.e. func (ah *appHandler) IndexHandler(...) because we need to call the handler in ServeHTTP (i.e. ah.h(w,r)).

type appContext struct {
    db        *sqlx.DB
    store     *sessions.CookieStore
    templates map[string]*template.Template
}

type appHandler struct {
    handler func(w http.ResponseWriter, r *http.Request) (int, error)
    *appContext // Embedded so we can just call app.db or app.store in our handlers.
}

// In main() ...
context := &appContext{db: nil, store: nil}
r.Get("/", appHandler{context.IndexHandler, context}) 
...

This is also, most importantly, fully compatible with http.Handler so we can still wrap our handler struct with generic middleware like so: gzipHandler(appHandler{context.IndexHandler, context}).

(I'm still open to other suggestions however!)


Update

Thanks to this great reply on Reddit I was able to find a better solution that didn't require passing two references to my context instance per-request.

We instead just create a struct that accepts an embedded context and our handler type, and we still satisfy the http.Handler interface thanks to ServeHTTP. Handlers are no longer methods on our appContext type but instead just accept it as a parameter, which leads to a slightly longer function signature but is still "obvious" and easy to read. If we were concerned about 'typing' we're breaking even because we no longer have a method receiver to worry about.

type appContext struct {
    db    *sqlx.DB
    store *sessions.CookieStore
    templates map[string]*template.Template

type appHandler struct {
    *appContext
    h func(a *appContext, w http.ResponseWriter, r *http.Request) (int, error)
}

func (ah appHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    // We can now access our context in here.
    status, err := ah.h(ah.appContext, w, r)
    log.Printf("Hello! DB: %v", ah.db)
    if err != nil {
        log.Printf("HTTP %d: %q", status, err)
        switch status {
        case http.StatusNotFound:
            // err := ah.renderTemplate(w, "http_404.tmpl", nil)
            http.NotFound(w, r)
        case http.StatusInternalServerError:
            // err := ah.renderTemplate(w, "http_500.tmpl", nil)
            http.Error(w, http.StatusText(status), status)
        default:
            // err := ah.renderTemplate(w, "http_error.tmpl", nil)
            http.Error(w, http.StatusText(status), status)
        }
    }
}

func main() {
    context := &appContext{
        db:    nil,
        store: nil,
        templates: nil,
    }

    r := web.New()
    // We pass a reference to context *once* per request, and it looks simpler
    r.Get("/", appHandler{context, IndexHandler})

    graceful.ListenAndServe(":8000", r)
}

func IndexHandler(a *appContext, w http.ResponseWriter, r *http.Request) (int, error) {
    fmt.Fprintf(w, "db is %q and store is %q\n", a.db, a.store)
    return 200, nil
}
like image 33
elithrar Avatar answered Oct 27 '22 11:10

elithrar