Writing the silliest Go API ever — memdec
I’ve been interviewing with a few companies the past month, and as part of these interviews, the other day I was asked to write an API as a take-home code challenge. I had a lot of fun doing it, so I thought I’d make a series of articles/videos on the subject.
I’m obviously not gonna use the exact same API from the interview, which left me with the question of what to do for these.
I then came up with the silliest, greatest idea I ever had!
First, some background:
There’s a thing called the World Memory Championship, where among other things people compete in memorizing the order of shuffled decks of cards.
I’ve learned the technique and it’s amazingly fun. No, really, I swear!
For real, it’s awesome. I love it. The problem is, it’s a pain to learn. And most of it isn’t even complicated, it’s just a lot of work. For some time now I’ve wanted to make an app to aid in this process, so let’s do that. Let’s write the API for this fictional app to use.
I’m calling it memdec
. Meaning memorize decks, memory decoder, whatever.
A warning: The goal here is to learn cool stuff and to have tons of fun doing it. I’m not gonna stress about doing things the best possible way they can technically be.
So let’s get to it.
The Dominic system
Before we can start writing any code, or even just the spec, we need to be clear on what we want to achieve.
My goal here is to aid in learning and practicing the Dominic system techniques. It’s a mnemonic system, that is, a file system for your brain. Let me explain briefly how it works:
- The main principle is that your brain isn’t good at all at recording loose information, but it is really good at remembering who did what, where.
- Hence we’re gonna encode information in that format. Our bytes are gonna be
who
, doingsome action
,someplace
. - For memorizing decks of cards that means we’ll need to start with card suits and card “numbers” (in quotes because we’ll include A, K, Q and J there).
- We’ll use suits and numbers to create a quick alphabet, which we can then turn into initials, which we can then associate with people and actions.
- For example, ♣8 in my system is CK, and that to me is Cosmo Kramer.
- Every person should also have a unique, corresponding action. I always picture Kramer smoking and drinking simultaneously.
- Lastly, we need a route. That’s just a sequence of places with a well defined order. It could be the path from your house to the nearest market, for example, or anything else. I use a walk-through of the Las Vegas strip from Fallout: New Vegas.
In sum, our smallest unit of action is someone, doing something, someplace. Then you just string those together along your route ’til you run out of cards. That’s not only infinitely easier than memorizing loose information, it’s also a lot of fun. (And for brevity, I usually use two pairs of person+action per location in a route.)
Let’s see this in action, take the sequence ♥Q, ♥7, ♦2, and ♠J. To me that’s Mozart (♥Q) moonwalking (♥7) towards an amused David Bowie (♦2), who’s eating cereal (♠J). Let’s say those are cards 45–48 in the deck, that means they’ll be doing that at the entrance of The Tops Casino.
And just to clarify. Those are my characters, performing the actions I chose, along my route. When you do it you’ll create your own alphabet, so it’ll be all tailored to you.
Anyway, enough of this for now. I’ll make a video of this whole process in action to better illustrate sometime. For now, watch this for a demonstration:
Features
Now that we know what we want to work with, let’s get down to specifics.
When we’re done with our app, I want it to be able to:
- Do the basic “memorize a deck of cards thing.” That is: create a deck, shuffle it, give me the cards one by one, time how long I took to memorize them, and then let me guess what the cards were and tell me how accurate my guesses were.
- Help users create the scaffolding for the “main operation” above. That is, provide tools to make it easier to create the initials, the characters, the actions, the routes. These are a pain to create the first time, so let’s make that easier.
- Have user accounts, save people’s learning progress, and then show them how they’re getting better, which cards they get wrong the most, and all sorts of stats like that.
- With that in hand we can have leaderboards and fancy social things.
That’s it for the API. That’s all I can possibly see us doing for now. Once that is done the back-end will be finished, and it’ll be time for front-end work.
Structure
This is obviously gonna be a multi-part series. For the foundational part of the project, I want to stick to four following core functions. User stories:
- As a user, I can create a new, shuffled deck.
- As a user, I can draw the next card, and keep doing that ’til I’ve seen them all.
- As a user, I can fetch info about a deck.
- As a developer, I can list all decks created. We’re gonna use this for testing during development, but it shouldn’t be available for end-users later.
And then we can tack the fancy stuff on top of this later. So with these above in mind, we’re gonna need:
Models:
Our decks will need to be uniquely identifiable, hold a bunch of shuffled cards, and we need to have a way of knowing which card we need to show next.
Cards only need a suit and number.
Let’s start with that. package models
:
You might have noticed some bson there, which is usually associated with MongoDB — more on that below. And those tags between back-ticks are used to make the field names lowercase in JSON and BSON — in the Go struct they need to be capitalized to indicate they’re public.
Controller:
Our controller needs to deal with the main functionality from our user stories, that is: create a deck, show info from a deck, show next card, and list all.
But before we get our hands dirty, some planning:
- Both the
show info
and theshow card
functions will need to fetch the deck object from the database. So let's have an extra function to do just that and avoid writing the same code twice. I'm calling itfetchDeck
. - Creating a new deck is a bit of a pain. There’s a whole bit of logic for shuffling, then a whole bit of logic from creating a new, unique ID, and then the usual stuff of pushing it to the database and dealing with HTTP. I’ll split these into three:
newID
for the ID,freshDeck
for shuffling, and then justCreate
for the rest. - Also we need a controller object to keep the database connection in.
So let’s just sketch them out, without any content for now. package ctrl
:
And in case you’re wondering, (c *Controller)
means that function is a method of the Controller
type.
Database:
For the database we’ll be using Mongo, and the reason is… because.
I don’t know. It’s easy, I’ve used it before, and I have an mLab account just sitting there. 🤷
We can re-think it later, but for now, package db
:
Most of it should be self-explanatory. IsUnique
will be called by newID
in the controller.
Main:
And lastly, our main package should be pretty simple. It’ll be a http.NewServeMux()
call, then a bunch of HandleFunc
s to route the requests to the right functions in our controller, and a http.ListenAndServe()
call at the end.
We’ll get to it when it’s time.
Here’s a structure overview:
memdec
├── ctrl
│ └── ctrl.go
├── db
│ └── db.go
├── models
│ └── models.go
└── main.go
Code
Now for the actual code. I wrote a quick prototype which we’re gonna use to get started. It’s far from perfect and we’ll probably refactor the whole thing ten times over as the series goes along.
Oh, and I’m omitting imports for brevity. You can see full finished product on the But will it blend? section below, but most of our code will import mgo
and our package models
, with the controller probably importing our database package as well.
Anyway, let’s get to the code!
package db
We’ll have six functions here: Init
, GetDeck
, AddDeck
, IsUnique
, IncrementLastShown
, and GetAllDecks
. Starting from the simplest:
func Init
starts a session with the Mongo driver and checks for errors. Pretty simple. The arg
there should be in mongodb://user:password@yourdatabase.com:12345/dbname
format.
You’ll be seeing db.DB("memdec").C("decks")
everywhere. It means we're choosing the database "memdec", and the collection "decks" within it. Other than that it's pretty simple again: func AddDeck
inserts a deck object into that collection, and returns an error if there’s any.
func GetAllDecks
asks the database to find an empty map (read: everything), and returns all results as a list of decks.
func GetDeck
asks the database for a deck with a matching id
, and returns it.
func IsUnique
checks whether the randomly generated ObjectId we'll get in our controller (below!) has already been used in the database. It's probably really super unlikely, but eh, I'd rather be safe.
The Limit
method makes it so I get one result, max. Ideally we'll get zero, so if we get one it'll return false already, no need for more. The Count
method gives us the number of results, which given the constraint we just saw, will have only two possible values: zero or one.
func IncrementLastShown
is interesting. The database call is similar to the other ones: we're finding a deck based on the id
, then applying a modification. That modification is a bson map, and the way it works is we're taking the modification described in the plusOne
map, and outputting the end result to deckCheck
—which we're not really using for anything. (Is there an Apply
that returns nothing?)
And for plusOne
, our mgo.Change
object, we're creating an update that increases the lastshownindex
field by one. ReturnNew
means deckCheck
will be populated with the new value, not the pre-existing one.
package ctrl
Now the controller is where the actual meat and potatoes are!
For starters we’ll need the Controller object, an initializer for it (NewController), and seven methods: Create, newID, freshDeck, Info, NextCard, fetchDeck, and ListAllDecks. These will process the HTTP requests we receive, make calls to package db
, and return the results via HTTP.
I’ll try and go from the simplest parts to the more complex ones:
Controller
is an object that carries our database session information.
func NewController
returns a new controller...
func ListAllDecks
makes a call to GetAllDecks
from package db
, and sends the results back as JSON in the HTTP response.
func Info
takes the last element from the URL (e.g. www.address.com/not_this/nope/this_one!) and fetches a deck that has that id
. We'll see function fetchDeck
below. Then it sends the results back with http.ResponseWriter
.
func fetchDeck
checks that the argument supplied is a valid ObjectId, then fetches from the database whatever deck holds it.
func NextCard
is slightly more convoluted than what we've seen so far.
First it grabs the id
and gets the corresponding deck from the database.
Then it checks whether all cards have already been shown. If they have we reply saying there are zero cards remaining, and that’s that.
Otherwise it will call IncrementLastShown
to tell the database that we're about to show the next card; and then send the actual next card back to the user, along with the number of cards remaining.
It bears repeating: the way we’re doing things people are only allowed to see each card once. And once you’ve seen them all, that’s it. No repeats.
func Create
was supposed to be super convoluted, but we're breaking it down into parts. So here we're getting a new deck with freshDeck
, and passing it on to our database with db.AddDeck
. If everything works correctly, we show the user the id
of their new deck.
func freshDeck
. In parts: First we make an empty deck object. Then we create a new id
for it with func newID
, which we'll see below. Next iterate through every suit and number to populate our deck with all 52 cards, and lastly we shuffle it.
That shuffling code is a bit convoluted — we’ll probably swap it later for the new rand.Shuffle
method that was introduced in Go 1.10.
func newID
has nothing new: it creates a new ObjectId, then uses db.IsUnique
to check that it is... unique. The reason it’s wrapped in a for loop is that we wanna keep generating id
s ’til we come up with an unique one (and there’s no while in Go).
main
And finally, our main function brings everything together:
- Initialize the database.
- Get a new controller.
- Create a new router.
- Route HTTP requests to the corresponding functions in the controller.
- Sit down, grab some coffee, and wait for HTTP connections.
But will it blend?
It should work now. Here’s the repo with the lot of it all together.
Let’s see:
That makes sense since we didn’t set and endpoint for /
.
Now let’s try creating a deck:
Cool. That big ugly number there is our deck ID.
Now let’s draw a card:
Yip. All good.
And let’s grab the whole deck:
Perfect.
I mean, according to plan. When we’re finished users won’t be able to see the full cards list until after they’re done guessing what they memorized. But it’s good for now.
You might have noticed though, that all our errors are panic(err)
. That's ugly, we'll fix it next time. Here's what happens:
When we send an incorrect request like that (it was expecting an ObjectId, not a “its-gonna-crash”), it panics. Thankfully it’s just that one thread that panics, not the whole app, but still.
Here’s what it looks like on the other terminal:
Yip. Panic.
Oh, and if you get an error like the one below when you’re trying to run this at home, it’s because you need to change the default mongodb://user:password@yourdatabase.com:12345/dbname
for actual working credentials.
It’s not difficult to run mongo on your machine if you’d like. I use mLab for convenience; they have a free tier.
Next steps
We got our first working prototype! Alright! Yeah!
While this was super fun to write, it’s far from what it should be. So let’s see what we can improve next:
- We need to add timestamp fields to our decks so we can time how long it took the user to memorize all the cards.
- We could add flags to make our program more convenient to use.
- And we could also keep our mongo credentials in a separate file.
- There’s also the issue that right now our controller depends on that
package db
specifically. Meaning if we wanna switch it for one full of fake calls to use for testing, it'll be a hassle. So we might wanna mess around with dependency injection there. - - I have just learned while proof-reading this article that the
id
s generated byfunc NewObjectId
are guaranteed to be unique (source, source). So we can get rid of that wholenewID
&IsUnique
mess I wrote. I won’t fix this now — we learn from our mistakes and I’ll let you learn from mine — but we’ll get to it next time. - And a real nerdy nit: When we’re adding cards to
newDeck.Cards
there onfreshDeck
, we're reallocating the underlying array a bunch of times and that's inefficient. It bugs me. It's an easy fix but I wanna take some time to explain the why of it.
So let’s get to that next, and afterwards we’ll see how it goes.
Thanks for reading, and see you next time!
If you enjoyed this article please share it, and make sure to subscribe to dEfective Go!