RESTful web service in Go using Goji

Although net/http provides all necessary abstractions to create web services out of the box, there are alternative packages providing additional features and allowing to minimize boilerplate code. Today we will have a look at how to implement a web service with Goji.

Example of Goji web service

Let's start with a working example. You can get if from GitHub.

package main

import (  
        "encoding/json"
        "fmt"
        "net/http"

        "goji.io"
        "goji.io/pat"
        "golang.org/x/net/context"
)

type book struct {  
        ISBN    string "json:isbn"
        Title   string "json:name"
        Authors string "json:author"
        Price   string "json:price"
}

var bookStore = []book{  
        book{
                ISBN:    "0321774639",
                Title:   "Programming in Go: Creating Applications for the 21st Century (Developer's Library)",
                Authors: "Mark Summerfield",
                Price:   "$34.57",
        },
        book{
                ISBN:    "0134190440",
                Title:   "The Go Programming Language",
                Authors: "Alan A. A. Donovan, Brian W. Kernighan",
                Price:   "$34.57",
        },
}

func main() {  
        mux := goji.NewMux()
        mux.HandleFuncC(pat.Get("/books"), allBooks)
        mux.HandleFuncC(pat.Get("/books/:isbn"), bookByISBN)
        mux.UseC(logging)
        http.ListenAndServe("localhost:8080", mux)
}

func allBooks(ctx context.Context, w http.ResponseWriter, r *http.Request) {  
        jsonOut, _ := json.Marshal(bookStore)
        fmt.Fprintf(w, string(jsonOut))
}

func bookByISBN(ctx context.Context, w http.ResponseWriter, r *http.Request) {  
        isbn := pat.Param(ctx, "isbn")
        for _, b := range bookStore {
                if b.ISBN == isbn {
                        jsonOut, _ := json.Marshal(b)
                        fmt.Fprintf(w, string(jsonOut))
                        return
                }
        }
        w.WriteHeader(http.StatusNotFound)
}

func logging(h goji.Handler) goji.Handler {  
        fn := func(ctx context.Context, w http.ResponseWriter, r *http.Request) {
                fmt.Printf("Received request: %v\n", r.URL)
                h.ServeHTTPC(ctx, w, r)
        }
        return goji.HandlerFunc(fn)
}

If we run the code it will start server listening on "localhost:8080". The server will handle two paths and return json with all books for /books request and json with a book with specified ISBN for /books/{isbn} one.

For example, http://localhost:8080/books/0321774639 will return:

{"ISBN":"0321774639","Title":"Programming in Go: Creating Applications for the 21st Century (Developer's Library)","Authors":"Mark Summerfield","Price":"$34.57"}

Additionally the service logs all requested URLs to standard output. For the previous example, it will log:

Received request: /books/0321774639  

Analyzing code

Goji API looks very similar to net/http, while it adds some useful features including Context from net/context, middleware and more convenient parameter parsing.

Context

Looking at handler function declaration in Giji, we will notice a Context parameter:

func allBooks(ctx context.Context, w http.ResponseWriter, r *http.Request)  

Besides storing request variables, Context works as a temporary storage of any other data that could be needed while serving request. We don't use this feature in our example, but in production service this could be very handy. Context is so convenient that some languages, like Java, make it a part of Request object itself.

Handlers

Registering handlers in Goji is very similar to net/http:

mux := goji.NewMux()  
mux.HandleFuncC(pat.Get("/books"), allBooks)  
mux.HandleFuncC(pat.Get("/books/:isbn"), bookByISBN)  
...
http.ListenAndServe("localhost:8080", mux)  

We create mux, add handlers with specified paths and run server passing host:port and mux as parameters. Please note a convenient way to define a path variable, which will be parsed by Goji and added to Context:

pat.Get("/books/:isbn")  

so that instead of Request it can be easier consumed from Context:

isbn := pat.Param(ctx, "isbn")  
Middleware

If you never used middleware before, it can be conceived as a wrapper around handler. Middleware allows to execute any code before, after or even instead of a handler. In our example there is only one logging middleware, which prints requested URL and relays to handler for further processing. Registering middleware is as simple as:

mux.UseC(logging)  

Production web services usually register multiple middlewares, i.e. authentication/authorization, logging, metrics.

Bottom line

Goji could be a good choice for developing RESTful web services. It plays nice with net/http from standard library while bringing to the table Context, middleware and more convenient parsing of path parameters.

comments powered by Disqus