Software Development

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 :)

Subscribe
Notify of
guest

This site uses Akismet to reduce spam. Learn how your comment data is processed.

0 Comments
Oldest
Newest Most Voted
Inline Feedbacks
View all comments
Back to top button