Learn Golang with me — Playing with Time

Picture of a fancy clock
Image credit: Brooke-Campbell on Wunderstock (license)

Welcome to part three of a series of articles where we create a pomodoro timer as a way to learn Golang.

In the previous article we added a system notification that would work regardless of what operating system you were on, and it would speak to you if you were on a Mac.

I've had a few meetings now where it's suddenly declared “Get back to work” followed by an awkward silence where everyone looks at me.

I think I need to fix that…

I had thought that my next improvement to the pomodoro timer was going to be adding a GUI. That was a “nice to have”, this is definitely a “need”.

The solution

Currently I'm quite fortunate in that I only have two regular meetings a day, and I know what time they're at. So my next upgrade is going to be the ability to add “quiet times” where the visual notification will still show, but there won't be any awkward audible alerts.

Implementing quiet times

Let's keep it simple for now and just define some times in a json file that the program can read. I have fifteen minutes until my next regular meeting, so let's get to it so we can test it!

Here are the times I want to be quiet. Sorry for any Americans reading — 24 hour clock just makes so much more sense.

// quietTimes.json

[
  {
      "start": "11:00",
      "end": "11:30"
  },
  {
      "start": "14:30",
      "end": "15:00"
  }
]

In the previous update we added a conditional to check if we were on a mac before trying to use the say api. We're going to add !isQuietTime() to the conditional here:

if os == "darwin" && !isQuietTime() {
   go exec.Command("say", message).Output()
}

Now all we need to do is implement this function! We just need to read our json and compare the current time to the time in the json file.

How to read JSON in Go

The steps are pretty much the same as in any programming language. We want to:

  1. Open the file
  2. Read the file
  3. Assign the contents to a variable
  4. Close the file

Opening the file is easy and straightforward. We'll handle the error incase there are any issues (like misspelling the filename — doh!).

jsonFile, err := os.Open("quietTimes.json")
if err != nil {
    fmt.Println(err)
}
fmt.Println("Successfully Opened quietTimes.json")

Next we can defer closing the file before we forget about it. Using defer means that Go will wait until the surrounding function returns before executing.

defer jsonFile.Close()

Calling it before we do anything else means that if we have an error after this point, the file should still be closed.

In order to assign the contents to a variable, we need to read the file and get the byte slice value:

byteValue, _ := ioutil.ReadAll(jsonFile)

Now for the interesting part… The json package in Go has a method called Unmarshal which allows us to define an interface which will be used to understand (parse) the contents of the file.

We know that our JSON file is basically a list of start and end times, so we can define our interface like this:

type quietTimes []quietTime
type quietTime struct {
    Start string 
    End   string 
}

Then by calling the Unmarshal function we should get a slice of quietTime structs. If you're paying good attention you may notice that our json keys are lowercase whereas the keys of the struct start with a capital letter. The function is clever enough that it can match Start with start or even StArT.

var quietTimes quietTimes
json.Unmarshal(byteValue, &quietTimes)

The second argument to Unmarshal needs to be a pointer, so we're using the & notation. A pointer in Go is a reference to where the value is stored in memory. Using & will give us the memory location instead of the actual value itself. As a quick aside, you can run this code on play.golang.org :

a := "test"
fmt.Println(a) // test
fmt.Println(&a) // 0xc00009e210

Back to our program, if we print out quietTimes, it gives us our data as expected:

fmt.Printf("\n%+v\n", quietTimes)
// [{Start:11:00 End:11:30} {Start:14:30 End:15:00}]

We're doing some interesting string formatting here. \n simply creates a new line so that we can see our message, and %+v is well described in the docs:

%v	the value in a default format
	when printing structs, the plus flag (%+v) adds field names

Checking the time against our quiet times

Now that we have a Go slice with our data in it, we should be able to do a time comparison to see if we're in the quiet time window or not.

First we need to convert our string to a Time. Since we've got our time in a standard format of HH:MM, we can split the string to get the hour and minute values, and then use those to build a new Time.

// quietTimes.go

// s = "11:30"
func getTimeFromString(s string) time.Time {
	ct := time.Now()
	t := strings.Split(s, ":")
	hour, _ := strconv.Atoi(t[0])
	min, _ := strconv.Atoi(t[1])
	return time.Date(ct.Year(), ct.Month(), ct.Day(), hour, min, 0, 0, ct.Location())
}

This method simply converts the string to two variables hour and min and then uses that in the standard construction of a new Time.

Now we can use some really nice Time functions in Go to check if we're in the right time window:

// quietTimes.go

ct := time.Now()
isQuietTime := false
for _, quietTime := range quietTimes {
  quietTimeStart := getTimeFromString(quietTime.Start)
  quietTimeEnd := getTimeFromString(quietTime.End)
  if ct.After(quietTimeStart) && ct.Before(quietTimeEnd) {
    isQuietTime = true
  }
}

I really like the Go Time package. It's really easy and intuitive to understand, which is great considering the pain I've had in JavaScript working with time.

That's us done - It works! No more awkward meetings for me! Here's a view of the full quietTimes.go file that we created in this article.

// quietTimes.go

package main

import (
	"encoding/json"
	"fmt"
	"io/ioutil"
	"os"
	"strconv"
	"strings"
	"time"
)

type quietTimes []quietTime

type quietTime struct {
	Start string
	End   string
}

func isQuietTime() bool {
	jsonFile, err := os.Open("quietTimes.json")
	if err != nil {
		fmt.Println(err)
	}
	fmt.Println("Successfully Opened quietTimes.json")
	defer jsonFile.Close()
	byteValue, _ := ioutil.ReadAll(jsonFile)

	var quietTimes quietTimes
	json.Unmarshal(byteValue, &quietTimes)
	fmt.Printf("\n%+v\n", quietTimes)

	ct := time.Now()
	isQuietTime := false
	for _, quietTime := range quietTimes {
		quietTimeStart := getTimeFromString(quietTime.Start)
		quietTimeEnd := getTimeFromString(quietTime.End)
		if ct.After(quietTimeStart) && ct.Before(quietTimeEnd) {
			isQuietTime = true
		}
	}

	fmt.Println(isQuietTime)
	return isQuietTime
}

func getTimeFromString(s string) time.Time {
	ct := time.Now()
	t := strings.Split(s, ":")
	hour, _ := strconv.Atoi(t[0])
	min, _ := strconv.Atoi(t[1])
	return time.Date(ct.Year(), ct.Month(), ct.Day(), hour, min, 0, 0, ct.Location())
}

If you're following along with the pomodoro series, you can find the code at this point in time on GitHub.

. . .

Previous 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

Next item in series:

Learn Golang with me - Go GUI

Follow along as we take our simple pomodoro timer and make it a graphical application in a few simple steps

06 Feb 2022
Image of tomato

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.