Learn Golang with me

I'll take you through creating a simple pomodoro timer, explaining my decisions along the way.

Three tomatoes on a vine
Photo by Alex Ghizila on Unsplash

The app

Let's start with it's most basic requirements:

  • Count down from 25 minutes and then alert the user
  • Count down from 5 minutes and then alert the user
  • Repeat indefinitely

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.

First steps

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.

gif of Program running
Program running (gif has been sped up)

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:

Program running with output every second
Program running with output every second

Making something happen after 25 minutes

This is ok for now, there's a few things that I'm thinking at this point:

  • Maybe we'd want to add minutes and seconds to our output so that it's easier to understand (who knows off the top of their head that 25 minutes is 1500 seconds?)
  • Maybe there's a way to update the same line in the output so that we're not constantly adding new lines to tell the time?
  • We probably want it counting down rather than up

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!

Program running with alerts shown
Program running with alerts shown (gif has been sped up)

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.

Next steps

I'd like to make some of these improvements:

  • Count down from our “alert time”
  • Display time in minutes and seconds
  • Change time on the same line
  • Set our desired time intervals from command line

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.

Refactoring

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.

Count down from our “alert time”

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.

Countdown working
Countdown working (gif has been sped up)

Display time in minutes and seconds

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.

Overwriting previous line doesn't work well when you get into single digits
Overwriting previous line doesn't work well when you get into single digits (gif has been sped up)

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)
Padded numbers look much better
Padded numbers look much better (gif has been sped up)

Specifying our time intervals from the command line

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)
}

Further improvements

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:

  • Give it a GUI
  • Have the GUI pop to the front of the screen when transitioning modes
  • Show the countdown timer on the menu bar
  • Have a settings screen so that you can specify the timings and the sounds that are made
  • Pause/skip/reset timer for interval

This article is long enough, so I'll save these changes for another time.

What have we learned?

In this article we have seen examples of using Go to:

  • handle time
  • execute shell commands
  • have an infinite loop
  • create a new type
  • create receiver functions
  • create pointer receiver functions
  • format a string

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:

Learn Golang with me - How to create a system alert using Golang

In this article I highlight two main methods I found for creating a system alert using Go.

12 Oct 2021
Image of Megaphone

Richard Bell

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.