Hello and welcome back to this series of posts where I document my journey into building a simple time tracker in Golang. In this blog post, we'll take a look into what I've worked on in the past couple of months for the project, what I struggled with, and what I learned.
Struggle #1: Installing Dep on Windows
The first struggle I faced was installing dep
on windows. For those who don't know, dep
is a dependency management tool for Go, just like npm
and bundler
.
As documented in the README of the repository, to install dep
on windows, you should download a tarball from a specific link, but that link is broken, responding with a 404. There is currently an issue opened about this, which I bumped but sadly no one has yet had the time to take a look into it.
The README also says that for any other platforms you can use a simple install script:
curl https://raw.githubusercontent.com/golang/dep/master/install.sh | sh
When I initially run this command on my machine, I kept getting the error:
Installation requires your GOBIN directory C/bin to exist
So, I needed to set up a bin folder for my go environment. I ended up creating a bin
bolder in my GOPATH
folder, like:
mkdir $GOPATH\\bin
Now the script above runs just fine! Now, to run dep
I needed to run:
$ $GOPATH\\bin\\dep init
$ $GOPATH\\bin\\dep ensure
The init
command is similar to npm init
, creating an empty Gopkg.toml
file in the current directory. The dependencies of the project can be specified in that file (refer to the documentation page to understand how it works).
Voila. I can now start coding/designing!
Struggle #2: Building the CLI skeleton
For the initial version of the tool, I decided a simple CLI application would be good enough. Before trying any solution/library from other people, I tried to build my own, and it wasn't that difficult to understand the basics. Golang's documentation helped me here.
All you need is the flag standard library. You can build powerful CLI apps just with this library, which is awesome. Initially, I built a simple CLI app with several subcommands, using as templates the commands I described in the previous blog post, where they didn't do anything useful, it was meant purely to test out the flag lib.
Then, figuring I'd want to build something more robust (with helper messages, command descriptions, etc) and deciding that I didn't want to re-invent the wheel, I started looking online for Golang open-source CLI app builders. I stumbled upon two: Cobra and Urfave/cli. I ended up using Cobra for no particular reason: it was just the first one I used, and I stuck with it.
I built the skeleton of the app quite quickly, which was nice because it meant I could just focus on the core logic of my application right away.
Now, I realize I wrote in the previous post that I wanted to use as little dependencies as possible. I do not intend to add any other dependency, at the time of this writing, besides Cobra (unless, of course, I want to extend the app and use other persistence mechanisms, like a database).
After writing the skeleton of the CLI, here's what it looks like when I type go run main.go help
(I didn't have to write the help
command since that is provided by default by Cobra
):
$ go run main.go help
With go-tt, you can easily track the time you spend on different activities throughout your day.
go-tt provides several reports outlining the time you have spent in all your registered activities.
The goal of this small app is to help you fight procrastination,
by making you aware of where you chose to spend your time.
Usage:
tt [command]
Available Commands:
add Adds a new activity
del Deletes an activity
help Help about any command
list Lists all activities
start Starts an activity
Flags:
-h, --help help for tt
Use "tt [command] --help" for more information about a command.
(Not all commands are implemented at this time of writing). It's starting to look like a real CLI app!
Struggle #3: Designing the Persistence Module
Before diving into writing any code, I needed to think about how was I going to structure the persistence module of the app. The persistence module is the module that handles the logic of storing/reading activity data from some internal persistence mechanism. The details of that implementation should be hidden from the outside. I didn't want to lock myself to a particular implementation so the module interface should only deal with, if possible, core domain models (such as Activity
). I decided to go with a simple solution: create an interface
to expose the API of the persistence module. I called it ActivityRepository
. Here's how the first version of that interface looks like:
type ActivityRepository interface {
Initialize() error
Update(core.Activity) error
List() []core.Activity
Delete(core.Activity) error
Start(core.Activity) error
Find(string) (*core.Activity, error)
}
The Initialize
method should handle the logic to initialize the whole repository. For a file-based implementation, it could be something like setting up the necessary files/folders in the user's file system, dealing with permissions on those files, etc. For a database based implementation, it could be setting the needed connections.
The remaining methods should be self-explanatory.
Struggle #4: Implementing a JSON repository
As I wrote in the previous post, the initial implementation for the persistence module would be based on a simple JSON data storage.
The first step was choosing a folder in the user's file systems to store the data. I set up the app so that on the first use, the folder .gott
would be created in the user's home folder (for that I used the method HomeDir
from the current OS user, which is fetched from the method Current
of the Golang's standard library os/user
).
I had to write a lot of utility methods to create/read/delete files from the filesystem, which turned out okay because I learned a lot about IO in Golang.
Struggle #5: Indexing activities with aliases
The next difficulty was finding a way to quickly fetch an activity given an alias. As I wrote in the previous post, an activity can have a name and an alias. This is so that the activity name can act more as a description, and the alias more as the identifier (perhaps I should rename these fields, uh). This way, the user doesn't have to remember exactly the activity name to start it, and can quickly reference it via its alias. The aliases are shown with the List
command, for example:
$ go run main.go add Activity1 -a=a1
$ go run main.go add Activity2 -a=act2
$ go run main.go add Activity3 -a=3
$ go run main.go list
activity1 (a1)
activity2 (act2)
activity3 (3)
How do we make sure that fetching the associated activity JSON file given an alias is as fast as possible (given our initial implementation using a JSON based repository)? The naive way would be to open each and every activity file in the data folder, read its contents into memory, fetching the Alias
key (for now, an activity JSON file contains: name
and alias
) and seeing if it matches the alias given as input. Of course, this is terribly slow.
Another option would be to give a specific file name structure to each file. Something like ActivityName_Alias.json
, and we could quickly identify the files by doing some regexp shenanigans, but this didn't quite felt right.
Another solution I thought about was using an in-memory database, like Redis, to store mappings between aliases and file names. But this would require an additional service to be running on the user's machine, which I think doesn't make sense for such a small CLI app.
So I ended up with creating a simple version of an index: a file called index.json
which contains the following structure:
{
"alias1": "/path/to/activityfile.json",
"alias2": "/path/to/activityfile.json"
}
Simple to use and implement. Of course, as always, this has the disadvantage of having to load the entire index file in memory and also the need to add/remove entries every time a new activity is created or deleted but, for now, it will suffice.
That'll be all for now! Thanks for reading! Remember you can take a look at the repository. If you have any suggestions on how I can improve the code, please open a ticket or better yet fork it and open a PR. Remember: I'm a noob at Golang, so I may have a lot of bad/inefficient code. But bare with me, I'm learning!
Top comments (2)
You can use
go mod
. It‘s the native dependency management since the newer versions of GoThanks! Haven't taken a look at that yet :) Will add it to my to-read list