Using Go to build a REST service on top of mongoDB
I’ve been following go (go-lang) for a while now and finally had some time to experiment with it a bit more. In this post we’ll create a simple HTTP server that uses mongoDB as a backend and provides a very basic REST API.
In the rest of this article I assume you’ve got a go environment setup and working. If not, look at the go-lang website for instructions (https://golang.org/doc/install).
Before we get started we need to get the mongo drivers for go. In a console just type the following:
go get gopkg.in/mgo.v2
This will install the necessary libraries so we can access mongoDB from our go code.
We also need some data to experiment with. We’ll use the same setup as we did in my previous article (http://www.smartjava.org/content/building-rest-service-scala-akka-http-a…).
Loading data into MongoDB
We use some stock related information which you can download from here (http://jsonstudio.com/wp-content/uploads/2014/02/stocks.zip). You can easily do this by executing the following steps:
First get the data:
wget http://jsonstudio.com/wp-content/uploads/2014/02/stocks.zip
Start mongodb in a different terminal
mongod --dbpath ./data/
And finally use mongoimport to import the data
unzip -c stocks.zip | mongoimport --db akka --collection stocks --jsonArray
And as a quick check run a query to see if everything works:
jos@Joss-MacBook-Pro.local:~$ mongo akka MongoDB shell version: 2.4.8 connecting to: akka > db.stocks.findOne({},{Company: 1, Country: 1, Ticker:1 } ) { "_id" : ObjectId("52853800bb1177ca391c17ff"), "Ticker" : "A", "Country" : "USA", "Company" : "Agilent Technologies Inc." } >
At this point we have our test data and can start creating our go based HTTP server. You can find the complete code in a Gist here: https://gist.github.com/josdirksen/071f26a736eca26d7ea4
In the following section we’ll look at various parts of this Gist to explain how to setup a go based HTTP server.
The main function
When you run a go application, go will look for the main function. For our server this main function looks like this:
func main() { server := http.Server{ Addr: ":8000", Handler: NewHandler(), } // start listening fmt.Println("Started server 2") server.ListenAndServe() }
This will configure a server to run on port 8000, and any request that comes in will be handled by the NewHandler() instance we create in line 64. We start the server by calling the server.listenAndServe() function.
Now lets look at our handler that will respond to requests.
The myHandler struct
Lets first look at what this handler looks like:
// Constructor for the server handlers func NewHandler() *myHandler { h := new(myHandler) h.defineMappings() return h } // Definition of this struct type myHandler struct { // holds the mapping mux map[string]func(http.ResponseWriter, *http.Request) } // functions defined on struct func (my *myHandler) defineMappings() { session, err := mgo.Dial("localhost") if err != nil { panic(err) } // make the mux my.mux = make(map[string]func(http.ResponseWriter, *http.Request)) // matching of request path my.mux["/hello"] = requestHandler1 my.mux["/get"] = my.wrap(requestHandler2, session) } // returns a function so that we can use the normal mux functionality and pass in a shared mongo session func (my *myHandler) wrap(target func(http.ResponseWriter, *http.Request, *mgo.Session), mongoSession *mgo.Session) func(http.ResponseWriter, *http.Request) { return func(resp http.ResponseWriter, req *http.Request) { target(resp, req, mongoSession) } } // implements serveHTTP so this struct can act as a http server func (my *myHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { if h, ok := my.mux[r.URL.String()]; ok { // handle paths that are found h(w, r) return } else { // handle unhandled paths io.WriteString(w, "My server: "+r.URL.String()) } }
Lets split this up and look at the various parts. The first thing we do is define a constructor:
func NewHandler() *myHandler { h := new(myHandler) h.defineMappings() return h }
When we call this constructor this will instantiate a myHandler type and call the defineMappings() function. After that it will return the instance we created.
How does the type look we instantiate:
type myHandler struct { // holds the mapping mux map[string]func(http.ResponseWriter, *http.Request) }
As you can we define a struct with a mux variable as a map. This map will contain our mapping between a request path and a function that can handle the request.
In the constructor we also called the defineMappings function. This funtion, which is defined on out myHandler struct, looks like this:
func (my *myHandler) defineMappings() { session, err := mgo.Dial("localhost") if err != nil { panic(err) } // make the mux my.mux = make(map[string]func(http.ResponseWriter, *http.Request)) // matching of request path my.mux["/hello"] = requestHandler1 my.mux["/get"] = my.wrap(requestHandler2, session) }
In this (badly named) function we define the mapping between incoming requests and a specific function that handles the request. And in this function we also create a session to mongoDB using the mgo.Dial function. As you can see we define the requestHandlers in two different ways. The handler for “/hello” directly points to a function, an the handler for the “/get” path, points to a wrap function which is also defined on the myHandler struct:
func (my *myHandler) wrap(target func(http.ResponseWriter, *http.Request, *mgo.Session), mongoSession *mgo.Session) func(http.ResponseWriter, *http.Request) { return func(resp http.ResponseWriter, req *http.Request) { target(resp, req, mongoSession) } }
This is a function, which returns a function. The reason we do this, is that we also want to pass our mongo session into the request handler. So we create a custom wrapper function, which has the correct signature, and just pass each call to a function where we also provide the mongo session. (Note that we also could have changed the ServeHTTP implementation we explain below)
Finally we define the ServeHTTP function on our struct. This function is called whenever we receive a request:
func (my *myHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { if h, ok := my.mux[r.URL.String()]; ok { // handle paths that are found h(w, r) return } else { // handle unhandled paths io.WriteString(w, "My server: "+r.URL.String()) } }
In this function we check whether we have a match in our mux variable. If we do, we call the configured handle function. If not, we just respond with a simple String.
The request handle functions
Lets start with the handle function which handles the “/hello” path:
func requestHandler1(w http.ResponseWriter, r *http.Request) { io.WriteString(w, "Hello world!") }
Couldn’t be easier. We simply write a specific string as HTTP response. The “/get” path is more interesting:
func requestHandler2(w http.ResponseWriter, r *http.Request, mongoSession *mgo.Session) { c1 := make(chan string) c2 := make(chan string) c3 := make(chan string) go query("AAPL", mongoSession, c1) go query("GOOG", mongoSession, c2) go query("MSFT", mongoSession, c3) select { case data := <-c1: io.WriteString(w, data) case data := <-c2: io.WriteString(w, data) case data := <-c3: io.WriteString(w, data) } } // runs a query against mongodb func query(ticker string, mongoSession *mgo.Session, c chan string) { sessionCopy := mongoSession.Copy() defer sessionCopy.Close() collection := sessionCopy.DB("akka").C("stocks") var result bson.M collection.Find(bson.M{"Ticker": ticker}).One(&result) asString, _ := json.MarshalIndent(result, "", " ") amt := time.Duration(rand.Intn(120)) time.Sleep(time.Millisecond * amt) c <- string(asString) }
What we do here is that we make use of the channel functionality of go to run three queries at the same time. We get the ticker information for AAPL, GOOG and MSFT and return a result to the specific channel. When we receive a response on one of the channels we return that response. So each time we call this service we either get the results for AAPL, for GOOG or for MSFT.
With that we conclude this first step into go-lang :)
Reference: | Using Go to build a REST service on top of mongoDB from our JCG partner Jos Dirksen at the Smart Java blog. |