I'll take you through creating a simple pomodoro timer, explaining my decisions along the way.
Let's start with it's most basic requirements:
A lot of people talk about MVP and I think it's been getting a bad wrap recently. I've seen it misused and abused, but I like the concept behind it when it's done well. So I'm going to build something super simple to achieve the requirements above, and then I'll look to iterate on it.
I think the first thing I'd want to do is print the number of seconds since the program started. So we'll need to store the time in memory when the program starts, and then have a simple loop that will print the difference in time on every iteration.
// main.go
package main
import (
"fmt"
"time"
)
func main() {
start := time.Now()
fmt.Println("Time at start:", start)
for {
elapsed := time.Now().Sub(start).Seconds()
fmt.Println("Time since start:", elapsed)
}
}
I've had nightmares about handling time in vanilla JavaScript. This is a real treat. I don't think there's anything too complex here. Note the for loop with no arguments — this is how you create an infinite loop in Go. Running this is a little intense as you can imagine as it spits out hundreds of lines every second.
I wonder if we can do something so that it just spits out the seconds. We could round the elapsed
value to the nearest whole number and then compare it to the previous value and only print it out if it has changed. This probably isn't going to be the most efficient program, but we'll get it working first and then worry about making it better.
// main.go
package main
import (
"fmt"
"time"
)
func main() {
start := time.Now()
fmt.Println("Time at start:", start)
prevElapsed := 0
for {
elapsed := int(time.Now().Sub(start).Seconds())
if elapsed != prevElapsed {
fmt.Println("Time since start:", elapsed)
prevElapsed = elapsed
}
}
}
There's a number of ways I could have rounded the float, but converting to an int
seemed to be the neatest option for me as it makes more sense to have our number of seconds be type int
. Converting from a float64
to int
basically just strips everything after the decimal point.
With our elapsed time now as an int
of the number of seconds, we can do a comparison to see if it's changed and only print if it has. Finally we set the prevElapsed
value to be the current elapsed
value so that the next iteration can perform the same check.
This looks much better:
This is ok for now, there's a few things that I'm thinking at this point:
However, these feel like minor issues that don't really affect the true purpose of the app. It's most basic requirement is to let us know when 25 minutes have passed. The fact that it's printing out the time since start right now is actually irrelevant for this requirement. So I'm going to ignore these for now and focus on having it alert the user after 25 minutes. Except I'm not going to wait 25 minutes each time to see if my code has worked. Let's make that 2 seconds instead while we're developing. It's long enough that I know that it's not firing instantly and hopefully long enough that I can see what happens when my context is on a different window.
This is a great argument for doing TDD at this point in time since I would hope to be able to mock out the time functions to give me some control over time itself (like my very own time stone). Even if I was approaching this as an experienced Go developer however, I think I'd want to test it manually as well. So let's continue with our manual approach for now.
I don't really know what I'm looking for so I'm just going to do some googling for “golang alert”…
I didn't find anything suitable on a quick search, but I did stumble across the OSX “say” api that I remember having a lot of fun with a few years ago. If you don't know what it is, it basically just reads out text audibly. So now I'm looking to see how you can run a shell command from Go.
// main.go
package main
import (
"fmt"
"os/exec"
"time"
)
func main() {
start := time.Now()
fmt.Println("Time at start:", start)
prevElapsed := 0
for {
elapsed := int(time.Now().Sub(start).Seconds())
if elapsed != prevElapsed {
fmt.Println("Time since start:", elapsed)
prevElapsed = elapsed
}
if elapsed == 2 {
exec.Command("say", "Times up suckah!").Output()
}
}
}
As you can see, it's pretty straightforward. I'll not show a gif this time since there's nothing for you to see, but I promise it works. Now I'm wondering if there's a more professional shell command that I could run to alert the user, maybe one that doesn't require a mac specific api. But I kinda like it if I'm honest. I'll come back to this later. First let's finish off our basic requirements by adding our second “alert” after an additional 5 minutes. Again for the sake of manually testing this, we'll change it to 3 seconds. So we will expect an alert at 2 seconds and another one at 5 seconds.
// main.go
package main
import (
"fmt"
"os/exec"
"time"
)
func main() {
start := time.Now()
fmt.Println("Time at start:", start)
prevElapsed := 0
inWorkMode := true
for {
elapsed := int(time.Now().Sub(start).Seconds())
if elapsed != prevElapsed {
fmt.Println("Time since start", elapsed)
prevElapsed = elapsed
if inWorkMode == true && elapsed == 2 {
alert("take a break")
start = time.Now()
inWorkMode = false
}
if inWorkMode == false && elapsed == 3 {
alert("Back to work")
start = time.Now()
inWorkMode = true
}
}
}
}
func alert(message string) {
fmt.Println(message)
exec.Command("say", message).Output()
}
I've extracted out the alert functionality and added in a Println so that you can see the output as well. Since we'll have two separate times to alert the user, we need to signal what “mode” they're in. I've done this with a simple boolean flag inWorkMode.
There's some repetition in this code that I'd like to tidy up, but first, proof that it's working!
OK so we can see that we've got a loop that will repeat indefinitely and alert the user at the end of every “mode”. In theory we could change the times to 25 minutes and 5 minutes and say we were done, but we're doing this to learn Go and I don't think we've really scratched the surface of what we can do. We've also already come up with some improvements we could make, and I've a few more ideas as well.
I'd like to make some of these improvements:
I'll do them in this order as well as I think the countdown will make it easier for us to see minutes and seconds if we're only running for a few seconds at a time. Eventually I'd like to create some sort of GUI for this, but for now, let's stick to our current interface.
Let's start by making the refactor we alluded to in the last section.
This is where the fun begins. Looking at the code we want to refactor, there's one part in particular which I feel should be it's own function:
start = time.Now()
inWorkMode = false
We do this to switch the mode between work and rest. We can change the boolean assignment to inWorkMode = !inWorkMode
and now we can use exactly the same code in each section, meaning that we can extract it! First let's create a new type which will hold these two values. We'll call it timer:
type timer struct {
start time.Time
inWorkMode bool
}
This let's us create a receiver function that we can use to update these values. A receiver function is a special type of function that will operate on the type that you define it to. You can think of it like a method on a class (except it's not quite the same)
func (t *timer) switchMode() {
t.start = time.Now()
t.inWorkMode = !t.inWorkMode
}
Hopefully this all looks straightforward to you, but you're probably wondering about that asterix next to the type timer
. This isn't how you'd normally write receiver functions, but we need to for our purposes. The asterix signifies that this will be a pointer receiver. Pointers are hard to get your head around. In this instance however what it means is that switchMode
will directly modify t
. It's a bit like an instance method.
It might make more sense with some code showing an example of it being used:
t := timer{
start: time.Now(),
inWorkMode: true,
}
fmt.Println(t.inWorkMode) // true
t.switchMode()
fmt.Println(t.inWorkMode) // false
If we didn't add that little asterix in there, the original t
value would not have been updated. Instead the function would have created a copy of t
and updated the values inside of it, meaning that the Println functions would have both returned true
. Not very useful for us…
With this type defined I'll be able to define a few more receiver functions and organise my code a little better. I've extracted out the logic to get the elapsed time as well.
// main.go
package main
import (
"fmt"
"os/exec"
"time"
)
const workDuration = 2
const restDuration = 3
func main() {
t := timer{
start: time.Now(),
inWorkMode: true,
}
prevElapsed := 0
for {
elapsed := t.getElapsedTimeInSeconds()
if elapsed != prevElapsed {
fmt.Println("Time since start", elapsed)
prevElapsed = elapsed
if t.inWorkMode && elapsed == workDuration {
alert("take a break")
t.switchMode()
}
if !t.inWorkMode && elapsed == restDuration {
alert("Back to work")
t.switchMode()
}
}
}
}
func alert(message string) {
fmt.Println(message)
exec.Command("say", message).Output()
}
// timer.go
package main
import "time"
type timer struct {
start time.Time
inWorkMode bool
}
func (t timer) getElapsedTimeInSeconds() int {
return int(time.Now().Sub(t.start).Seconds())
}
func (t *timer) switchMode() {
t.start = time.Now()
t.inWorkMode = !t.inWorkMode
}
You'll notice that since getElapsedTimeInSeconds
does not modify t
so it doesn't need to have a pointer receiver.
An interesting thing with Go is that you don't need to import your own files. So long as they're in the same package
you should be able to make use of them in any file. You do need to remember to include these extra files when you are running or building your program however. So for me running locally I need to type go run main.go timer.go
Now I think we can probably delegate the responsibility of the alert to the timer type and couple the alert message with the inWorkMode
flag.
func (t timer) alert() {
message := "Take a break"
if !t.inWorkMode {
message = "Back to work"
}
fmt.Println(message)
exec.Command("say", message).Output()
}
This means that in our main function we can simply call t.alert()
and it will alert with the correct message.
I think we can do two more small refactors and then we'll get back to feature development.
Our conditional logic is not the easiest to read. It's not terrible, but I think we can make it clearer by having two functions called shouldSwitchToWorkMode
and shouldSwitchToRestMode
. I think we'll be able to combine these, but let's not run before we can walk (even though that's what every baby actually tries to do).
const workDuration = 2
const restDuration = 3
func (t timer) shouldSwitchToWorkMode(elapsed int) bool {
return t.inWorkMode && elapsed == workDuration
}
func (t timer) shouldSwitchToRestMode(elapsed int) bool {
return !t.inWorkMode && elapsed == restDuration
}
Let's take the next step and make this into one function shouldSwitchMode
func (t timer) shouldSwitchMode(elapsed int) bool {
duration := workDuration
if !t.inWorkMode {
duration = restDuration
}
return elapsed == duration
}
Now our main function is looking nice and simple. Here are the two files now:
// main.go
package main
import (
"fmt"
"time"
)
func main() {
t := timer{
start: time.Now(),
inWorkMode: true,
}
prevElapsed := 0
for {
elapsed := t.getElapsedTimeInSeconds()
if elapsed != prevElapsed {
fmt.Println("Time since start", elapsed)
prevElapsed = elapsed
if t.shouldSwitchMode(elapsed) {
t.alert()
t.switchMode()
}
}
}
}
// timer.go
package main
import (
"fmt"
"os/exec"
"time"
)
type timer struct {
start time.Time
inWorkMode bool
}
func (t timer) getElapsedTimeInSeconds() int {
return int(time.Since(t.start).Seconds())
}
func (t *timer) switchMode() {
t.start = time.Now()
t.inWorkMode = !t.inWorkMode
}
func (t timer) alert() {
message := "Take a break"
if !t.inWorkMode {
message = "Back to work"
}
fmt.Println(message)
exec.Command("say", message).Output()
}
const workDuration = 2
const restDuration = 3
func (t timer) shouldSwitchMode(elapsed int) bool {
duration := workDuration
if !t.inWorkMode {
duration = restDuration
}
return elapsed == duration
}
There's still some tidy up we could probably do, but let's get back to feature work.
Our alert time is coupled to the inWorkMode
flag, so it would make sense for us to delegate the count down message to our timer type. I can create this function in the timer file and simply replace my fmt.Println
in main.go with a call to this function instead.
func (t timer) printTimeRemaining(elapsed int) {
if t.inWorkMode {
timeRemaining := workDuration - elapsed
fmt.Printf("Working for another %d seconds
", timeRemaining)
}
if !t.inWorkMode {
timeRemaining := restDuration - elapsed
fmt.Printf("Resting for another %d seconds
", timeRemaining)
}
}
There's some refactoring that could be done here, but we'll be changing this code a few more times, so we'll leave it for now and see if something comes naturally.
This should be straight forward. We'll change our two duration constants to be multiple minutes, and change the output slightly.
if t.inWorkMode {
timeRemaining := workDuration - elapsed
minutes := timeRemaining / 60
seconds := timeRemaining - minutes*60
fmt.Printf("Working: %d:%d
", minutes, seconds)
}
This code is going to be exactly the same for the other if block, so I'm going to do a small simple refactor here. I'll introduce a getDuration
function.
func (t timer) getDuration() int {
duration := workDuration
if !t.inWorkMode {
duration = restDuration
}
return duration
}
This means that we can update our shouldSwitchMode
function as well. However because of the message in the print statement, I'll need to do some logic around creating that depending on the mode. I'll create a getMode
function for now.
While I'm here, I'll quickly do the next feature as well. It turns out that modifying the previous line printed to is pretty easy. All you need to do is put a carriage return (\r
) at the start of the message (and we'll need to remove the new line we're adding at the end of course).
Here's the code at this point:
// main.go
package main
import (
"time"
)
func main() {
t := timer{
start: time.Now(),
inWorkMode: true,
}
prevElapsed := 0
for {
elapsed := t.getElapsedTimeInSeconds()
if elapsed != prevElapsed {
t.printTimeRemaining(elapsed)
prevElapsed = elapsed
if t.shouldSwitchMode(elapsed) {
t.alert()
t.switchMode()
}
}
}
}
// timer.go
package main
import (
"fmt"
"os/exec"
"time"
)
type timer struct {
start time.Time
inWorkMode bool
}
func (t timer) getElapsedTimeInSeconds() int {
return int(time.Since(t.start).Seconds())
}
func (t *timer) switchMode() {
t.start = time.Now()
t.inWorkMode = !t.inWorkMode
}
func (t timer) alert() {
message := "Take a break"
if !t.inWorkMode {
message = "Back to work"
}
fmt.Println(message)
exec.Command("say", message).Output()
}
const workDuration = 2 * 60
const restDuration = 3 * 60
func (t timer) shouldSwitchMode(elapsed int) bool {
return elapsed == t.getDuration()
}
func (t timer) getDuration() int {
duration := workDuration
if !t.inWorkMode {
duration = restDuration
}
return duration
}
func (t timer) getMode() string {
mode := "Work"
if !t.inWorkMode {
mode = "Rest"
}
return mode
}
func (t timer) printTimeRemaining(elapsed int) {
timeRemaining := t.getDuration() - elapsed
minutes := timeRemaining / 60
seconds := timeRemaining - minutes*60
fmt.Printf("
%v: %d:%d", t.getMode(), minutes, seconds)
}
It does mess up when the length of what you're printing changes, so I think we'll need to pad our minutes and seconds with a zero to avoid this.
I was getting ready to create a new padNumber
function like I've always had to do in JavaScript. However a quick Google search showed me that there's a really easy way to do this when printing a string.
When formatting a number we can specify a width and further to that we can choose to fill the empty width with a zero if we want to. It's as easy as this:
fmt.Printf("\r%v: %02d:%02d", t.getMode(), minutes, seconds)
For this we need to do a little bit of defensive programming. We want the user to run the program with two arguments, one for the work time and one for the rest time. If they don't provide both of these arguments we will default to using 25minutes and 5minutes respectively and print a message.
// main.go
package main
import (
"fmt"
"os"
"strconv"
"time"
)
const defaultWorkDurationInMins = 25
const defaultRestDurationInMins = 5
func main() {
workDurationInMins := defaultWorkDurationInMins
restDurationInMins := defaultRestDurationInMins
if len(os.Args) != 3 {
fmt.Println("Incorrect number of arguments, using default values of 25minutes working and 5minutes resting")
}
if len(os.Args) == 3 {
wdur, err1 := strconv.Atoi(os.Args[1])
rdur, err2 := strconv.Atoi(os.Args[2])
workDurationInMins = wdur
restDurationInMins = rdur
if err1 != nil || err2 != nil {
fmt.Println("Problem setting interval values, using default values of 25minutes working and 5minutes resting")
}
}
t := timer{
start: time.Now(),
inWorkMode: true,
workDuration: workDurationInMins * 60,
restDuration: restDurationInMins * 60,
}
prevElapsed := 0
for {
elapsed := t.getElapsedTimeInSeconds()
if elapsed != prevElapsed {
t.printTimeRemaining(elapsed)
prevElapsed = elapsed
if t.shouldSwitchMode(elapsed) {
t.alert()
t.switchMode()
}
}
}
}
// timer.go
package main
import (
"fmt"
"os/exec"
"time"
)
type timer struct {
start time.Time
inWorkMode bool
workDuration int
restDuration int
}
func (t timer) getElapsedTimeInSeconds() int {
return int(time.Since(t.start).Seconds())
}
func (t *timer) switchMode() {
t.start = time.Now()
t.inWorkMode = !t.inWorkMode
}
func (t timer) alert() {
message := "Take a break"
if !t.inWorkMode {
message = "Back to work"
}
fmt.Println(message)
exec.Command("say", message).Output()
}
func (t timer) shouldSwitchMode(elapsed int) bool {
return elapsed == t.getDuration()
}
func (t timer) getDuration() int {
duration := t.workDuration
if !t.inWorkMode {
duration = t.restDuration
}
return duration
}
func (t timer) getMode() string {
mode := "Work"
if !t.inWorkMode {
mode = "Rest"
}
return mode
}
func (t timer) printTimeRemaining(elapsed int) {
timeRemaining := t.getDuration() - elapsed
minutes := timeRemaining / 60
seconds := timeRemaining - minutes*60
fmt.Printf("
%v: %02d:%02d", t.getMode(), minutes, seconds)
}
We've got something that's pretty useful for my purposes right now. I've actually been using it for the past few days while I've been waiting to publish this article, and it works like a charm. Using it highlights some things that I would like to improve:
This article is long enough, so I'll save these changes for another time.
In this article we have seen examples of using Go to:
We've also seen an example of how you can progressively enhance a simple solution into something that's usable, and what you should focus on as you go.
If you'd like to follow along you can find the code at this point in time on GitHub.
Next item in series:
In this article I highlight two main methods I found for creating a system alert using Go.
Self taught software developer with 11 years experience excelling at JavaScript/Typescript, React, Node and AWS.
I love learning and teaching and have mentored several junior developers over my career. I find teaching is one of the best ways to solidify your own learning, so in the past few years I've been maintaining a technical blog where I write about some things that I've been learning.
I'm passionate about building a teams culture and processes to make it efficient and satisfying to work in. In many roles I have improved the quality and reliability of the code base by introducing or improving the continuous integration pipeline to include quality gates.