Time is one of the most challenging things to work with in any programming language. Go makes it as easy as possible. In this blog we'll teach you everything you need to know about working with time and how to write testable time-based code.

Working With Time in Golang


Time is one of the most challenging things to work with in any programming language. Go makes it as easy as possible. In this blog we’ll teach you everything you need to know about working with time and how to write testable time-based code.

The Basics: What Time is It?

You can get the current time as follows:

package main

import (
    "fmt"
    "time"
)

func main() {
    currentTime := time.Now()
    fmt.Println("Current Time:", currentTime)
}

This will output something like:

Current Time: 2024-09-19 15:04:05.123456789 -0700 PDT

Here, time.Now() returns the current local time, including the date, hour, minute, second, and nanoseconds.

Working with Timezones

The below code gets the current time in New York. The LoadLocation() function uses IANA Time Zone database names, allowing you to work with timezones easily.

package main

import (
    "fmt"
    "log"
    "time"
)

func main() {
    location, err := time.LoadLocation("America/New_York")
    if err != nil {
        log.Fatal(err)
        return
    }

    newYorkTime := time.Now().In(location)
    fmt.Println("New York Time:", newYorkTime)
}

Formatting Time in Golang

Go provides the ability to format time. The time.Format() method uses layout strings to control how the time is displayed. One unique aspect of Go’s time formatting is that it uses a reference time, so instead of using regular expression you pass in what you’d like it to look like. You can find all the options in the Go source code and documentation here.

package main

import (
    "fmt"
    "time"
)

func main() {
    currentTime := time.Now()
    formattedTime := currentTime.Format("2006-01-02 15:04:05")
    fmt.Println("Formatted Time:", formattedTime)
}

outputs:

Formatted Time: 2024-09-19 15:04:05

Parsing Time Strings

Another common scenario when working with time in Golang is parsing strings into time.Time objects. Go uses the same reference time for parsing, and you need to specify the exact layout of the string you’re parsing.

package main

import (
    "fmt"
    "log"
    "time"
)

func main() {
    timeString := "2024-09-19 14:00:00"
    layout := "2006-01-02 15:04:05"
    parsedTime, err := time.Parse(layout, timeString)
    if err != nil {
        log.Fatal(err)
        return
    }

    fmt.Println("Parsed Time:", parsedTime)
}

This example parses a date string into a time.Time object, making it easy to work with times provided by input.

Doing Calculations on time

A standard use-case is to find the difference between two times, usually to work out how long something took, or maybe in your system you have an event that fires in the future. This is easy with the time package.

package main

import (
    "fmt"
    "time"
)

func main() {
    currentTime := time.Now()
    oneHourLater := currentTime.Add(1 * time.Hour)
    fmt.Println("One hour later:", oneHourLater)

    tenMinutesAgo := currentTime.Add(-10 * time.Minute)
    fmt.Println("Ten minutes ago:", tenMinutesAgo)
}

Timers and Tickers

If you’re building an application that needs to execute code after a certain period or at regular intervals, Go provides time.Timer and time.Ticker for this purpose.

  • Timers: Fire once after a specified duration.
  • Tickers: Fire repeatedly at fixed intervals.

Here’s how you can use a time.Timer to wait for 2 seconds before running some code:

package main

import (
    "fmt"
    "time"
)

func main() {
    timer := time.NewTimer(2 * time.Second)
    <-timer.C
    fmt.Println("Timer expired!")
}

and here’s a ticker that runs every second:

package main

import (
    "fmt"
    "time"
)

func main() {
    ticker := time.NewTicker(1 * time.Second)
    for t := range ticker.C {
        fmt.Println("Tick at", t)
    }
}

Comparing Times

You might need to compare two times to determine whether one is before or after the other. The Before(), After(), and Equal() methods are useful here:

package main

import (
    "fmt"
    "time"
)

func main() {
    time1 := time.Now()
    time2 := time1.Add(2 * time.Hour)

    if time1.Before(time2) {
        fmt.Println("time1 is before time2")
    }

    if time2.After(time1) {
        fmt.Println("time2 is after time1")
    }

    if time1.Equal(time2) {
        fmt.Println("time1 is equal to time2")
    }
}

Testing Time

Testing time-based applications is really hard. I have used this library for years, and it’s never let me down.

The clockwork package provides two types of clocks:

RealClock: Uses the actual system clock.

FakeClock: A mock clock that allows you to manipulate time during tests.

To install it, run:

go get github.com/jonboulle/clockwork

then here’s how you could write some testable code that involves a clock:

package main

import (
    "fmt"
    "time"

    "github.com/jonboulle/clockwork"
)

// Scheduler struct with an embedded clock interface
type Scheduler struct {
    clock clockwork.Clock
}

// NewScheduler creates a new Scheduler with a provided clock
func NewScheduler(clock clockwork.Clock) *Scheduler {
    return &Scheduler{clock: clock}
}

// WaitForTimeout waits for the specified duration using the clock and returns when done
func (s *Scheduler) WaitForTimeout(d time.Duration) string {
    <-s.clock.After(d)
    return "Timeout reached"
}

// WaitUntil waits until a specified future time using the clock
func (s *Scheduler) WaitUntil(t time.Time) string {
    if s.clock.Now().Before(t) {
        <-s.clock.After(t.Sub(s.clock.Now()))
    }
    return "Reached specified time"
}

// GetTicks returns n tick intervals of 1 minute
func (s *Scheduler) GetTicks(n int) []string {
    ticker := s.clock.NewTicker(1 * time.Minute)
    ticks := []string{}
    for i := 0; i < n; i++ {
        t := <-ticker.Chan()
        ticks = append(ticks, fmt.Sprintf("Tick at %v", t))
    }
    ticker.Stop()
    return ticks
}

Notice how we put the clock interface on the struct?

this then allows us to write tests like this:

package main

import (
    "testing"
    "time"

    "github.com/jonboulle/clockwork"
)

func TestScheduler_WaitForTimeout(t *testing.T) {
    // Create a fake clock
    fakeClock := clockwork.NewFakeClock()

    // Create a new scheduler with the fake clock
    scheduler := NewScheduler(fakeClock)

    // Simulate waiting for 1 hour
    resultChan := make(chan string)
    go func() {
        resultChan <- scheduler.WaitForTimeout(1 * time.Hour)
    }()

    // Fast forward the clock by 1 hour
    fakeClock.Advance(1 * time.Hour)

    // Assert the result
    result := <-resultChan
    if result != "Timeout reached" {
        t.Errorf("Expected 'Timeout reached', got %v", result)
    }
}

func TestScheduler_WaitUntil(t *testing.T) {
    // Create a fake clock
    fakeClock := clockwork.NewFakeClock()

    // Create a new scheduler with the fake clock
    scheduler := NewScheduler(fakeClock)

    // Set a target time 2 hours into the future
    futureTime := fakeClock.Now().Add(2 * time.Hour)

    // Simulate waiting until the future time
    resultChan := make(chan string)
    go func() {
        resultChan <- scheduler.WaitUntil(futureTime)
    }()

    // Fast forward the clock by 2 hours
    fakeClock.Advance(2 * time.Hour)

    // Assert the result
    result := <-resultChan
    if result != "Reached specified time" {
        t.Errorf("Expected 'Reached specified time', got %v", result)
    }
}

func TestScheduler_GetTicks(t *testing.T) {
    // Create a fake clock
    fakeClock := clockwork.NewFakeClock()

    // Create a new scheduler with the fake clock
    scheduler := NewScheduler(fakeClock)

    // Simulate getting 3 ticks, advancing the clock by 3 minutes
    go func() {
        ticks := scheduler.GetTicks(3)
        if len(ticks) != 3 {
            t.Errorf("Expected 3 ticks, got %v", len(ticks))
        }
    }()
    for i := 0; i < 3; i++ {
        fakeClock.Advance(1 * time.Minute)
    }
}

This makes our testing code deterministic.

Wrapping Up

Working with time in Go is straightforward once you understand the basics. The time package provides a comprehensive set of tools for time manipulation, formatting, and parsing. Whether you’re dealing with time zones, formatting dates, or scheduling tasks, Go has you covered.