Physical Address
304 North Cardinal St.
Dorchester Center, MA 02124
Physical Address
304 North Cardinal St.
Dorchester Center, MA 02124
Following on from a recent post – Introduction to Go I want to look in more detail at using Go to build microservices. Go seems to be a popular language choice for this, so in this post (part 1 of 2), we will explore what it takes to get a basic web service up and running (not very much actually) and add some simple functionality.
For the purposes of learning, I am creating a very simple ‘To Do’ lists service. It will expose a CRUD REST API allowing items to be added, updated and removed. In part 2 I will hook this up to a MongoDB. This should provide a pretty good look at what Go has to offer out of the box.
For now the REST API only support adding and retrieving to do items.
I am using JetBrains GoLand, but this should work just fine with VSCode or just notepad and the command line. There seems to be some debate about project set up and as Go has evolved, modules seem to be the newer preferred choice. Based on this I have created a new project with a single module (advice is one module per repo). The module is named to match the GitHub repo, which is a Go convention. Within this I have created several packages to keep the code separated and clean.
So, let’s start up a web service, Go provides this out of the box and its very easy to set up.
func main() {
// Route the path to a function
http.HandleFunc("/todo", controllers.ProcessRequest)
log.Fatal(http.ListenAndServe(":8080", nil))
}
In a ‘controller’ package the ProcessRequest() function is defined. Important note: The function is naming convention is CamelCase with a capital letter at the start. This initially caught me out a cost me some time…
Coming from predominately Java, I automatically used CamelCase with a lowercase letter at the start, writing the function as processRequest – this is interpreted as a package private function in Go, only visible within the scope of package its defined in. To make functions publicly visible outside of their packages (part of the public API), they must start with a capital letter. Go has no concept of private beyond this.
Routing the request
package controller
import (
"encoding/json"
"github.com/mikej6502/todo-list-svc/database"
"github.com/mikej6502/todo-list-svc/model"
"net/http"
)
// GET all items in the to do list
func ProcessRequest(w http.ResponseWriter, r *http.Request) {
if r.Method == http.MethodGet {
listItems(w, r)
} else if r.Method == http.MethodPost {
addItems(w, r)
} else {
http.Error(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed)
}
}
While you can map paths to functions, the most specific path being matched, I can’t find a nicer way (or haven’t as yet) to map the HTTP verbs to specific functions, so this method effectively does this by routing the GET and POST requests. Get will return all items, whereas POST will create a new item.
The POST request will submit a block of json representing the item. The Go standard library makes it easy to convert the json to a struct.
The post request body
{
"title" : "Write a blog post on Go - Part 1",
"description" : "New blog post on writing a microservice using Go",
"completed" : false
}
In the model package I have defined a simple struct for the Item. Again, fields with a capital letter are public and lowercase private. Note there are no classes as such on Go, instead we have structs. There are no constructors that you might expect coming from Java.
type Item struct {
Id string
Title string
Description string
Completed bool
}
Map the http request body to a struct
decoder := json.NewDecoder(r.Body)
var item model.Item
err := decoder.Decode(&item)
Errors must be explicitly caught and assigned to a variable or ignored using an underscore. If assigned to a variable, it must be used – it’s a compiler error to have an unused variable in Go. I decided to catch the error and simply return a http status code 500 back to the client.
It’s just as simple to return a list of items as JSON for the GET request. I am storing the items in-memory for mow in a list (slice), to return them as json, it’s just…
// items stored in a list
var items []model.Item
var json, err = json.Marshal(items)
// write the json to the http.ResponseWriter to return the items as JSON
w.Write(json)
In part 2 we will hook up a MongoDB database, but for now I am happy with an in-memory datastore to get the functionality working. I will of course structure the code in such a way that makes it easy to switch out the In-memory database for MongoDB. To achieve this, we use an interface. The concept is the same as other OO languages such as Java or C#, but the execution is a little different.
For now, just two public API methods, one to add items and one to list all items.
type DataStore interface {
GetItems() []model.Item
AddItem(item model.Item)
}
In Go interfaces are automatically added to structs that define the methods, so no need to explicitly add the interface to the struct.
The concrete implementation of the interface – An In-Memory datastore
type InMemoryDataStore struct {
}
Unlike other languages, methods are not added within the struct itself, but outside. The function is defined as normal, but has an extra parameter – a message receiver. This takes some adjusting to when coming from say Java or C#, but you soon get used to it. Let’s keep it simple for now.
var items []model.Item
// func <message receiver> <method name> <method return type>
func (d InMemoryDataStore) GetItems() []model.Item {
return items
}
func (d InMemoryDataStore) AddItem(item model.Item) {
items = append(items, item)
}
In part 2, we will return multiple values from the functions, the second being an error value.
Using the interface is nice and easy from our controller.
// InMemory datastore conforming to the datastore interface
datastore := database.InMemoryDataStore{}
func listItems(w http.ResponseWriter, r *http.Request) {
var json, _ = json.Marshal(datastore.GetItems())
w.Write(json)
}
func addItems(w http.ResponseWriter, r *http.Request) {
decoder := json.NewDecoder(r.Body)
var item model.Item
err := decoder.Decode(&item)
if err != nil {
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
} else {
datastore.AddItem(item)
w.WriteHeader(http.StatusCreated)
}
}
The interface will allow us to add a new implementation of datastore, specifically for MongoDB (or any other store we choose), and with only a single line change we can swap out the implementation – nice and clean SOLID code.
That’s if for part 1, in part two we will hook up Mongo DB, this will be the only external library required (the MongoDB driver).
Once I got use to the language, I found it pretty easy and straightforward to implement this very basic web service. Go is designed to be a very simple and easy to learn programming language, with this in mind the creators have cut back on rich language features, favouring simple, easy to read code. You may in some cases write a little more code, but it should be adding to the overall readability.
As a modern language, Go provides a lot of core functionality right out of the box. For this service, I didn’t have to import a single 3rd party dependency. Contrast to Java, I would have needed an external build tool such as Maven, a library for JSON conversion and a library for the webserver, ok, Spring Boot would have done this (an excellent external famework), and with a lot of simplicity, but the point is with Go it’s all provided as standard. I am not favouring one over the other, just pointing out the differences.
Although only a few lines of code, compile time was very fast, another one of the stated aims of Go.
In part 2 lets hook up MongoDB and Dockerise the whole app, while taking advantage of the built-in cross-compilation feature.
The full code for this can be found in my repo: https://github.com/mikej6502/todo-list-svc