Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Accomplishing common App Engine handler tasks with Go

This is a best-practice question that probably doesn't have one correct answer. It seems that most of my handlers need to perform a number of common initialisation jobs before starting on work specific to the handler. Examples would be user auth, detecting locale and loading translated strings, checking memcached values, and so on.

It would seem reasonable to take care of some of these tasks within init, but most require the Http.Request or the appengine.Context. As far as I can see this leaves three choices:

  1. Implement ServeHTTP and add the ability to execute a custom init function at the end. Problem being, I wouldn't be able to use Gorilla mux which implements its own ServeHTTP.

  2. Use a forked version of mux (less than ideal).

  3. Put a startHandler function at the beginning of each and every handler throughout the app. Seems cumbersome, although I suppose it makes it plain exactly what's happening as opposed to 'hiding' common code in ServeHTTP.

What's the preferred way to take care of jobs common to all handlers? Am I missing another approach?


Here's a complete App Engine example of the approach outlined in minikomi's answer. It's also well-worth visiting Jeff Wendling's tutorial.

package app                                                                                                                                                                                                                                     

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

    "appengine" 
    "appengine/datastore" 

    "github.com/gorilla/context" 
    "github.com/gorilla/mux" 
)

type Config struct {
    DefaultLocale string 
    DefaultTimezone string 
}

type ContextKey int 

const (
    SiteConfig ContextKey = iota 
    // ... 
)

type InitHandler func(http.ResponseWriter, *http.Request, appengine.Context)

func (h InitHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    // All handler initialisation tasks go here 
    c := appengine.NewContext(r)
    k := datastore.NewKey(c, "Config", "site:config", 0, nil)
    config := new(Config)
    if err := datastore.Get(c, k, config); err != nil {
        log.Fatal("Couldn't read config from datastore: %s\n", err.Error())
    }
    context.Set(r, SiteConfig, config)

    // Finally, call the handler itself 
    h(w, r, c)
}

func init () {
    r := mux.NewRouter()
    r.Handle("/", InitHandler(home))  // Note: NOT r.HandleFunc!
    http.Handle("/", r)
}

func home(w http.ResponseWriter, r *http.Request, c appengine.Context) {
    site := context.Get(r, SiteConfig).(*Config)
    fmt.Fprintf(w, "Locale: %s, timezone: %s.", site.DefaultLocale, site.DefaultTimezone)
}

What threw me for a little while is the need to use router.Handle and not router.HandleFunc. Assuming an appropriate entity in the datastore, output looks like:

Locale: en_US, timezone: UTC.
like image 449
Rich Churcher Avatar asked Jan 16 '13 00:01

Rich Churcher


1 Answers

You can create a (func) type which has a ServeHTTP that does everything you need, and then internally calls the original function, and then convert your handlers to that type:

package main

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

type wrappedHandler func(w http.ResponseWriter, r *http.Request)

func (h wrappedHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
        log.Println("Do Other GAE Stuff")
        h(w, r)
}

func handler(w http.ResponseWriter, r *http.Request) {
        fmt.Fprintf(w, "Hi!")
}

func main() {
        http.Handle("/", wrappedHandler(handler))
        http.ListenAndServe(":8080", nil)
}

If you want to pass something in to the handler() func, you can add it to the signature eg:

type wrappedHandler func(w http.ResponseWriter, r *http.Request, conn *db.Connection)

func (h wrappedHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
        conn := db.CreateConnection();
        h(w, r, conn)
}


func handler(w http.ResponseWriter, r *http.Request, conn *db.Connection) {
        data := conn.AllTheData()
        fmt.Fprintf(w, data)
}
like image 133
minikomi Avatar answered Nov 16 '22 09:11

minikomi