A few months ago I blogged about creating a particle simulation in Go as a method of trying to learn the language. In order to expand on that, I tried to implement a more standard application - a simple set of web services supporting creation, listing, and deletion of messages.
Initial Set Up
In order to get started, the data structures needed to be created: a message, and a list of messages. Their implementation was trivial:
There’s a couple of items here that weren’t in my previous post, the json
tags in the struct, and also the use of packages.
The use of multiple packages helps keep related items grouped together, and separate concerns. In this simple example I’ve grouped by structs
. However it could be groups of similar structs, constants, functions, interfaces in any combination. For instance, a HTTP abstraction and its interfaces, structs and implementation can be implemented in an abstraction
package.
In terms of the json
tag, it simply informs Go’s JSON package to map between the struct values and the JSON values. For instance, JSON object {"id": 1, "sender": "Test", "message": "Test Message"}
would map to the ID
, Sender
, and Message
fields respectively in the Message
struct.
Creating a Store
Now that the data structure has been defined, so can the implementation of storing messages, and the functions that would interact with the store.
The functions that were most interesting to implement from a newcomer’s perspective were Add
and Remove
. Add
takes a message, and sets an ID (which is an incremented value whenever Add
is called, for a unique key), then appends it to the store. Remove
was a tad more complicated - given an ID, it finds the message with that ID and removes it from the slice. The removal was done in an interesting way: it takes the slice of everything before that item (store[:index]
), then appends everything after that item store[index+1:]...
in a spread-like operation, adding each argument to a variadic function.
Creating the Services
Routing
The service routing was incredibly simplistic. The logic of the routing was choosing the handler based on the request method – GET
requests should return a list, whereas POST
would create a new message and DELETE
would remove a message.
All that was left was to start the server in the main
function, and define what route it should listen to.
This listens on port 8080
for any request - the /
path is a catch-all handler.
List
Now that the store is implemented, the services could be created. Starting with listing the current messages:
The bulk of the work was done by marshalling the storage into a JSON format. Then, all that needed to be done was add the required headers and send the JSON. It also became apparent later on that this marshalling would be done many times, so I thought it best to write a utility to manage that:
The utility does the same as the above – takes an input, generates JSON, and writes the response. However, it comes with the bonus of having any response handling done in the one place - leading to consistency, and code reuse. It also led to simplifying the request handlers – the function for retrieving the list became much shorter and simpler:
I also added a utility for handling error states for much the same reason – to reduce the repeated code and simplify the handlers.
Again, relatively simple implementation - it just logs a message, the error (if applicable), and sends the provided error code and message to the client. It turned out to be a very useful utility to add to increase code readability, too.
In action, sending a GET request returned this response, as expected:
Create/Delete
Create and delete are fairly similar, so it makes sense to cover them together. They both take some input from a POST or DELETE request body and manipulate the message list. However, there’s a few subtle differences. Creation takes the input of a message object, minus the ID. Then, validation is performed on the unmarshalled JSON to ensure those fields are provided:
There was a new struct added for sending and receiving IDs, as this made more sense than responding with the same data sent in the request body plus an ID field.
If the fields are not present in the request body, then they are initialised by default to be empty values, like ""
or 0
. In this case, the message shouldn’t be added as not everything’s provided. For instance, sending:
…would result in a bad request response. In successful cases, it would return the ID:
Deletion is similar:
Again, validation is performed to ensure that the id
field was present on the request body. The removal also handles the case when the ID is not present in the list of messages - the return value of storage.Remove
- in which case, it returns a bad request.
Conclusions
Having written these few endpoints, I do feel that they could be further simplified. For example, the handlers are provided with the raw ResponseWriter
and Request
values, where it is potentially more useful (in this limited case, at least) to provide the request body to the handling functions and then use the return value to indicate a successful response. This could be a bit more limiting in a more complex application with additional routing and logic, etc.
Nevertheless, I’ve been impressed at the experience of writing Go web services. I was using the standard HTTP library and it seems powerful enough for most cases. Again, writing the code has been enjoyable, and the code produced was easy to read and clean. I feel like separating the code up into multiple packages and separating the concerns also helped achieve this, as it made the code slightly more explicit in what it was actually doing (for example, storage.Add
is more understandable than AddToStorage
). Go is a language worth keeping an eye on, it’s used extensively in Monzo and will likely go on to bigger and better things.
If you want to have a look at the code, it is available on GitHub.