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.