Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Building Modern Web Apps in Go Part 2

David Nix
November 19, 2015

Building Modern Web Apps in Go Part 2

This was a presentation I gave at the Denver Go Meetup in November 2015.

My goal is to teach others and myself how to build a robust, modern web application in Go. My hope is that startups will turn to Go instead of Rails or Django when building web apps.

Disclaimer: This is an exploration. I am not an expert on the subject. I’ve built microservices in Go but have yet to build a monolith similar to Rails or Django.

David Nix

November 19, 2015
Tweet

More Decks by David Nix

Other Decks in Programming

Transcript

  1. MODERN WEB APP COMPONENTS1 Routing, Middleware, Request Scoped Data, Authentication,

    Data Modeling (Database), Asset Pipeline, Templates/Views, Static files, Graceful shutdown, Transactional Email, Live Reload, Logging 1 Non-exhaustive list
  2. THIS IS NOT A MODERN WEB APP package main import

    "net/http" func main() { http.HandleFunc("/", hello) http.ListenAndServe(":8080", nil) } func hello(w http.ResponseWriter, r *http.Request) { w.Write([]byte("hello, world!")) }
  3. I LIED, THIS IS NOT ENOUGH // net/http package type

    Handler interface { ServeHTTP(ResponseWriter, *Request) }
  4. In a middleware chain such as: type Middleware func(h http.Handler)

    http.Handler How do I pass data from one handler to the next? > handle to the database > authenticated user (i.e. current user) > request id
  5. By using or passing a "context"2 from one handler to

    the next. 2 "Context": when programmers don't know what to name something.
  6. OPTION 1: GLOBAL, THREADSAFE3 DATA STRUCTURE package context // ...

    var ( mutex = mutex sync.RWMutex data = make(map[*http.Request]map[string]interface{}) ) 3 Synonymous with Goroutine-safe
  7. package context // ... func Set(r *http.Request, key string, val

    interface{}) { mutex.Lock() defer mutex.Unlock() if data[r] == nil { data[r] = make(map[string]interface{}) } data[r][key] = val } func Get(r *http.Request, key string) interface{} { mutex.RLock() defer mutex.RUnlock() if ctx := data[r]; ctx != nil { return ctx[key] } return nil }
  8. USAGE IN TYPICAL MIDDLEWARE import ( "path/to/context" "net/http" ) func

    setCurrentUser(h http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { user := getCurrentUser(r) if user == nil { w.WriteHeader(http.StatusUnauthorized) return } context.Set(r, "currentUser", currentUser) h.ServeHTTP(w, r) }) }
  9. Pros > Can be used with any middleware chain Cons

    > Performance bottleneck 4 > Resource Leaks (have to clean up after yourself) > Global state 4 Measure first.
  10. RECOMMENDED LIBRARY https://github.com/gorilla/context > When paired with Gorilla's mux, don't

    have to worry about cleaning up. > The key in the inner map is an interface{}
  11. OPTION 2: CUSTOM MIDDELWARE CHAIN import ( "golang.org/x/net/context" // yup,

    Go Team also provides a context "net/http" ) type CtxHandler interface { CtxServeHTTP(ctx context.Context, w http.ResponseWriter, r *http.Request) } type CtxHandlerFunc func(ctx context.Context, w http.ResponseWriter, r *http.Request) func (fn CtxHandlerFunc) CtxServeHTTP(ctx context.Context, w http.ResponseWriter, r *httpRequest) { fn(ctx, w, r) } type Middleware func (h CtxHandler) CtxHandler
  12. Example usage import ( "golang.org/x/net/context" "net/http" ) func setCurrentUser(h CtxHandler)

    CtxHandler { return CtxHandlerFunc(func(ctx context.Context, w http.ResponseWriter, r *http.Request) { user := getCurrentUser(r) if user == nil { w.WriteHeader(http.StatusUnauthorized) return } newCtx := context.WithValue(ctx, "currentUser", user) // note: different api h.CtxServeHTTP(newCtx, w, r) }) }
  13. But serve mux wants http.Handler import ( "golang.org/x/net/context" "net/http" )

    // implement http.Handler func (ctxh CtxHandlerfunc) ServeHTTP(w http.ResponseWriter, r *http.Request) { ctx := context.Background() ctxh(ctx, w, r) }
  14. Or import ( "golang.org/x/net/context" "net/http" ) func contextHandler(h CtxHandler) http.HandlerFunc

    { return func(w http.ResponseWriter, r *http.Request) { ctx := context.Background() h.CtxServeHTTP(ctx, w, r) } }
  15. Pros > Completely customizable > No global state Cons >

    More work > Not compatible with regular http.Handler's > But you can wrap http.Handler
  16. OPTION 3: USE A LIBRARY Notably: > Goji - https://github.com/zenazn/goji

    > Gin - https://github.com/gin-gonic/gin > Echo - https://github.com/labstack/echo
  17. CUSTOM HANDLERS // Goji type HandlerFunc func(C, http.ResponseWriter, *http.Request) //

    Gin type HandlerFunc func(*Context) // Echo type HandlerFunc func(*Context) error
  18. From Echo's Godoc type Context func NewContext(req *http.Request, res *Response,

    e *Echo) *Context func (c *Context) Bind(i interface{}) error func (c *Context) Error(err error) func (c *Context) File(path, name string, attachment bool) (err error) func (c *Context) Form(name string) string func (c *Context) Get(key string) interface{} func (c *Context) HTML(code int, format string, a ...interface{}) (err error) func (c *Context) JSON(code int, i interface{}) (err error) func (c *Context) JSONIndent(code int, i interface{}, prefix string, indent string) (err error) func (c *Context) JSONP(code int, callback string, i interface{}) (err error) func (c *Context) NoContent(code int) error func (c *Context) P(i int) (value string) func (c *Context) Param(name string) (value string) func (c *Context) Path() string func (c *Context) Query(name string) string func (c *Context) Redirect(code int, url string) error func (c *Context) Render(code int, name string, data interface{}) (err error) func (c *Context) Request() *http.Request func (c *Context) Response() *Response func (c *Context) Set(key string, val interface{}) func (c *Context) Socket() *websocket.Conn func (c *Context) String(code int, format string, a ...interface{}) (err error) func (c *Context) XML(code int, i interface{}) (err error) func (c *Context) XMLIndent(code int, i interface{}, prefix string, indent string) (err error)
  19. Pros > All the benefits when using 3rd party library

    Cons > All the dangers when relying on 3rd party library
  20. I recommend Echo. Why? > Great documentation and examples >

    Centralized http error handling > Segregated Middleware
  21. RUMOR MILL FOR GO 1.7 Go team suggested that http.Request

    struct may have a public field called... drumroll please Context
  22. DO I REALLY HAVE TO DEAL WITH INTERFACE{}? var (

    mutex = mutex sync.RWMutex data = make(map[*http.Request]map[string]interface{}) ) func Get(r *http.Request, key string) interface{} { mutex.RLock() defer mutex.RUnlock() if ctx := data[r]; ctx != nil { return ctx[key] } return nil }
  23. OPTION 1: UNWRAP IT func getCurrentUser(ctx context.Context) models.User { user,

    ok := ctx.Get("currentUser").(models.User) if !ok { // but probably fine to leave off "ok" and just let it panic } return user }
  24. OPTION 2: CUSTOM DATA STRUCTURE func Env(ctx context.Context) *Env {

    env, ok := ctx.Get("env").(*Env) if !ok { env = &Env{} ctx.Set("env", env) return env } return env }
  25. type Env struct { // note: unexported mu sync.RWMutex currentUser

    *models.User } func (e *Env) setCurrentUser(user *models.User) { e.mu.Lock() e.currentUser = user e.mu.Unlock() } func (e *Env) getCurrentUser() *models.User { e.mu.RLock() defer e.mu.RUnlock() return e.currentUser }