Customising Your Own Web Framework In Go

25
Customising Your Own Web Framework in Go 20 January 2015 Jonathan Gomez Engineer, Zumata

Transcript of Customising Your Own Web Framework In Go

Page 1: Customising Your Own Web Framework In Go

Customising Your Own WebFramework in Go20 January 2015

Jonathan GomezEngineer, Zumata

Page 2: Customising Your Own Web Framework In Go

This Talk

Overview - Intro to serving requests with http/net - Customising Handlers - Writing Middleware - Ecosystem

Key takeaways - Compared with Ruby/Node.js, mainly using the standard library is considered normal- Interfaces and first-class functions make it easy to extend functionality - Ecosystem of libraries that work alongside http/net is growing

Page 3: Customising Your Own Web Framework In Go

Intro to Serving Requests with http/net

Page 4: Customising Your Own Web Framework In Go

Serving Requests via Standard Lib (1/4)

package main

import "net/http"

func handlerFn(w http.ResponseWriter, r *http.Request) { w.Write([]byte(̀Hello world!̀))}

func main() { http.HandleFunc("/", handlerFn) http.ListenAndServe("localhost:4000", nil)}

ListenAndServe - creates server that will listen for requests

Each request spawns a go routine: go c.serve()

Page 5: Customising Your Own Web Framework In Go

Serving Requests via Standard Lib (2/4)

ServeMux matches incoming request against a list of patterns (method/host/url)

ServeMux is a special kind of Handler which calls another Handler

Handler interface

type Handler interface { ServeHTTP(ResponseWriter, *Request)}

Page 6: Customising Your Own Web Framework In Go

Serving Requests via Standard Lib (3/4)

Request handling logic in ordinary function func(ResponseWriter, *Request)

func final(w http.ResponseWriter, r *http.Request) { w.Write([]byte("OK"))}

Register the function as a Handler on DefaultServeMux

http.Handle("/", http.HandlerFunc(final))

Also can:

http.HandleFunc("/", final)

Page 7: Customising Your Own Web Framework In Go

Serving Requests via Standard Lib (4/4)

func(ResponseWriter, *Request)

ResponseWriter interface

type ResponseWriter interface { Header() Header Write([]byte) (int, error) WriteHeader(int)}

Request struct

type Request struct { Method string URL *url.URL Header Header Body io.ReadCloser ContentLength int64 Host string RemoteAddr string ...}

Page 8: Customising Your Own Web Framework In Go

Customising Handlers

Page 9: Customising Your Own Web Framework In Go

Demo: Customising Handlers - DRY Response Handling (1/3)

type appHandler struct { h func(http.ResponseWriter, *http.Request) (error)}

func (ah appHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { err := ah.h(w, r) if err != nil { switch err := err.(type) { case ErrorDetails: ErrorJSON(w, err) default: ErrorJSON(w, ErrorDetails{"Internal Server Error", "", 500}) } }}

In app code we might extend this further:

Add error types and respond differently.

e.g. warn vs error-level log, send alerts, increment error metrics

Page 10: Customising Your Own Web Framework In Go

Demo: Customising Handlers - DRY Response Handling (2/3)

type ErrorDetails struct { Message string ̀json:"error"̀ Details string ̀json:"details,omitempty"̀ Status int ̀json:"-"̀}

func (e ErrorDetails) Error() string { return fmt.Sprintf("Error: %s, Details: %s", e.Message, e.Details)}

func ErrorJSON(w http.ResponseWriter, details ErrorDetails) {

jsonB, err := json.Marshal(details) if err != nil { http.Error(w, err.Error(), 500) return }

w.Header().Set("Content-Type", "application/json") w.WriteHeader(details.Status) w.Write(jsonB)}

Page 11: Customising Your Own Web Framework In Go

Demo: Customising Handlers - DRY Response Handling (3/3)

Use of special struct and special handler function to satisfy Handler interface

http.Handle("/", appHandler{unstableEndpoint})

Reduce repetition, extend functionality.

func unstableEndpoint(w http.ResponseWriter, r *http.Request) (error) {

if rand.Intn(100) > 60 { return ErrorDetails{"Strange request", "Please try again.", 422} }

if rand.Intn(100) > 80 { return ErrorDetails{"Serious failure", "We are investigating.", 500} }

w.Write([]byte(̀{"ok":true}̀)) return nil} Run

Page 12: Customising Your Own Web Framework In Go

Demo: Customising Handlers - Avoiding Globals

Allows injecting dependencies rather than relying on global variables.

type Api struct { importantThing string // db *gorp.DbMap // redis *redis.Pool // logger ...}

type appHandler struct { *Api h func(*Api, http.ResponseWriter, *http.Request)}

func (ah appHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { ah.h(ah.Api, w, r)}

func myHandler(a *Api, w http.ResponseWriter, r *http.Request) { w.Write([]byte("2015: Year of the " + a.importantThing))} Run

Page 13: Customising Your Own Web Framework In Go

Writing Middleware

Page 14: Customising Your Own Web Framework In Go

Middleware: Why?

Abstract common functionality across a set of handlers

Bare minimum in Go:

func(next http.Handler) http.Handler

Typical uses of middleware across languages/frameworks: - logging - authentication - handling panic / exceptions - gzipping - request parsing

Page 15: Customising Your Own Web Framework In Go

Demo: Middleware Example (Panic Recovery)

func recoveryMiddleware(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { defer func() { if err := recover(); err != nil { log.Println("Recover from error:", err) http.Error(w, http.StatusText(500), 500) } }() log.Println("Executing recoveryMiddleware") next.ServeHTTP(w, r) })

}

func final(w http.ResponseWriter, r *http.Request) { log.Println("Executing finalHandler") panic("walau!") w.Write([]byte("OK"))} Run

Page 16: Customising Your Own Web Framework In Go

Demo: Chaining Middleware

func middlewareOne(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { log.Println("-> Executing middlewareOne") next.ServeHTTP(w, r) log.Println("-> Executing middlewareOne again") })}

Calling chain of middleware

http.Handle("/", middlewareOne(middlewareTwo(http.HandlerFunc(final))))

func middlewareTwo(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { log.Println("---> Executing middlewareTwo") next.ServeHTTP(w, r) log.Println("---> Executing middlewareTwo again") })} Run

Page 17: Customising Your Own Web Framework In Go

Chaining Middleware - Alternate Syntax

3rd Party Library: Alice

Manages middleware with the standard function signature

Nice syntax for setting up chains used in different endpoints

chain := alice.New(middlewareOne, middlewareTwo)http.Handle("/", chain.Then(finalHandler))

Our example

noAuthChain := alice.New(contextMiddleware, loggerMiddleware)authChain := alice.New(contextMiddleware, loggerMiddleware, apiKeyAuthMiddleware)adminChain := alice.New(contextMiddleware, loggerMiddleware, adminAuthMiddleware)

Page 18: Customising Your Own Web Framework In Go

Demo: Creating Configurable Middleware

e.g. Pass the dependency on *AppLogger

var logger *AppLogger = NewLogger()loggerMiddleware := simpleLoggerMiddlewareWrapper(logger)http.Handle("/", loggerMiddleware(http.HandlerFunc(final)))

func simpleLoggerMiddlewareWrapper(logger *AppLogger) func(http.Handler) http.Handler { return func(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {

startTime := time.Now()

next.ServeHTTP(w, r)

endTime := time.Since(startTime) logger.Info(r.Method + " " + r.URL.String() + " " + endTime.String()) }) }} Run

Page 19: Customising Your Own Web Framework In Go

Demo: Customising ResponseWriter (1/3)

type ResponseWriter interface { Header() http.Header Write([]byte) (int, error) WriteHeader(int)}

ResponseWriter as an interface allows us to extend functionality easily

Example:

Step 1: Create a struct that wraps ResponseWriter

type responseWriterLogger struct { w http.ResponseWriter data struct { status int size int }}

Record data that would be otherwise be untracked.

Page 20: Customising Your Own Web Framework In Go

Demo: Customising ResponseWriter (2/3)

Step 2: Define methods required for implicit satisfaction

func (l *responseWriterLogger) Header() http.Header { return l.w.Header()}

func (l *responseWriterLogger) Write(b []byte) (int, error) {

// scenario where WriteHeader has not been called if l.data.status == 0 { l.data.status = http.StatusOK } size, err := l.w.Write(b) l.data.size += size return size, err}

func (l *responseWriterLogger) WriteHeader(code int) { l.w.WriteHeader(code) l.data.status = code}

Page 21: Customising Your Own Web Framework In Go

Demo: Customising ResponseWriter (3/3)

func specialLoggerMiddlewareWrapper(logger *AppLogger) func(http.Handler) http.Handler { return func(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {

startTime := time.Now()

w2 := &responseWriterLogger{w: w} next.ServeHTTP(w2, r)

logger.Info(r.Method + " " + r.URL.String() + " " + time.Since(startTime).String() + " status: " + strconv.Itoa(w2.data.status) + " size: " + strconv.Itoa(w2.data.size))

}) }} Run

Page 22: Customising Your Own Web Framework In Go

Growing Middleware Ecosystem

Excerpt from Negroni Github page graceful: (https://github.com/stretchr/graceful) graceful HTTP Shutdown

oauth2: (https://github.com/goincremental/negroni-oauth2) oAuth2 middleware

binding: (https://github.com/mholt/binding) data binding from HTTP requests into structs

xrequestid: (https://github.com/pilu/xrequestid) Assign a random X-Request-Id: header to each request

gorelic: (https://github.com/jingweno/negroni-gorelic) New Relic agent for Go runtime

Mailgun's Oxy stream: (http://godoc.org/github.com/mailgun/oxy/stream) retries and buffers requests and responses

connlimit: (http://godoc.org/github.com/mailgun/oxy/connlimit) Simultaneous connections limiter

ratelimit: (http://godoc.org/github.com/mailgun/oxy/ratelimit) Rate limiter

Page 23: Customising Your Own Web Framework In Go

Other Web Framework Components

Routing & Extracting URL Params - standard library can be inflexible - regex for extracting url params can feel too low level - plenty of third party routers, e.g. Gorilla mux

func ShowWidget(w http.ResponseWriter, r *http.Request) { vars := mux.Vars(r) teamIdStr := vars["team_id"] widgetIdStr := vars["widget_id"] ...}

Request-specific context - sharing data between items in middleware chain and final handler - solutions involve either global map, or per-request map/structs using customhandlers/middleware

Page 24: Customising Your Own Web Framework In Go

Web frameworks vs Build on top of standard library?

Time/expertise to build what you need? Too much re-inventing? Your optimisation vs framework optimisation? Performance? Does performance order of magnitude matter? How much magic do you want? Compatibility with net/http / ecosystem? Framework interchangeability?

Martini -- 6.1k (https://github.com/go-martini/martini)

Revel -- 4.7k (https://github.com/revel/revel)

beego -- 3.7k (https://github.com/astaxie/beego)

goji -- 1.9k (https://github.com/zenazn/goji)

gin -- 1.9k (https://github.com/gin-gonic/gin)

negroni -- 1.8k (https://github.com/codegangsta/negroni)

go-json-rest -- 1.1k (https://github.com/ant0ine/go-json-rest)

Gorilla/mux -- 1.1k (https://github.com/gorilla/mux)

Tiger Tonic -- 0.8k (https://github.com/rcrowley/go-tigertonic)

Gocraft/web -- 0.6k (https://github.com/gocraft/web)

Page 25: Customising Your Own Web Framework In Go

Thank you

Jonathan GomezEngineer, [email protected] (mailto:[email protected])

@jonog (http://twitter.com/jonog)