Time in Go: A primer

April 26, 2016

Time in Go: A primer

Time is of the essence.

This initially started out as merely a few snippets of code regarding the formatting of dates, but then I decided that it is probably of more use if I publish it.

I extended the snippets slightly, and added some examples of commonly used functionality within the time library.

The reason why I have these snippets is because, well, Go’s date/time formatting is a bit odd. While I agree it increases readability, it takes some getting used to, after many years of formatting dates in pretty much the same way across different languages.

A short list of subjects that will be (partly) covered:

  1. Formatting
  2. Locations
  3. Parsing
  4. Comparison & calculations

Formatting

Let’s start with formatting. Formatting is based on the following reference time:

Mon Jan 2 15:04:05 -0700 MST 2006

What does this mean? This means if you would want a specific date formatted like: 26-04-2016, you would use the reference time like this: 02-01-2006.

It’s pretty straight forward right? You rewrite the reference time in the format you desire, and Go will spit out the correct date for you.

package main

import (
    "fmt"
    "time"
)

func main() {
    t := time.Now()
    fmt.Println(t.Format("02-01-2006"))
    fmt.Println(t.Format("02-01-2006 15:04:05"))
}

// Output:
// 26-04-2016
// 26-04-2016 08:03:23

These are two formats I commonly use myself. Aside from using your own formats, the time package provides you with a wide choice of standardized formats.

const (
    ANSIC       = "Mon Jan _2 15:04:05 2006"
    UnixDate    = "Mon Jan _2 15:04:05 MST 2006"
    RubyDate    = "Mon Jan 02 15:04:05 -0700 2006"
    RFC822      = "02 Jan 06 15:04 MST"
    RFC822Z     = "02 Jan 06 15:04 -0700" // RFC822 with numeric zone
    RFC850      = "Monday, 02-Jan-06 15:04:05 MST"
    RFC1123     = "Mon, 02 Jan 2006 15:04:05 MST"
    RFC1123Z    = "Mon, 02 Jan 2006 15:04:05 -0700" // RFC1123 with numeric zone
    RFC3339     = "2006-01-02T15:04:05Z07:00"
    RFC3339Nano = "2006-01-02T15:04:05.999999999Z07:00"
    Kitchen     = "3:04PM"
    // Handy time stamps.
    Stamp      = "Jan _2 15:04:05"
    StampMilli = "Jan _2 15:04:05.000"
    StampMicro = "Jan _2 15:04:05.000000"
    StampNano  = "Jan _2 15:04:05.000000000"
)

Which can be used like this:

package main

import (
    "fmt"
    "time"
)

func main() {
    t := time.Now()
    fmt.Println(t.Format(time.RFC822Z))
}

// Output: 26 Apr 16 08:10 +0200

As you can see my time offset is displayed in the last example, which brings us to locations.

Locations

As you can see in the previous example my time offset is displayed, this is because by default (when using time.Now()) Go uses your local machine time. So how do you handle users with different locations? You would want to display the correct times depending on your user’s location.

Generally, you’ll first want to bring back every time to a specific location. Go provides the UTC() function out of the box, this is what I’ll use.

Officially UTC is not a timezone but a standard. Hence the name Location in the time package.

package main

import (
    "fmt"
    "time"
)

func main() {
    t := time.Now().UTC()
    fmt.Println(t.Format(time.RFC822Z))
}

// Output: 26 Apr 16 06:10 +0000

This is something you might store somewhere in a database. Now when you want to display this time to someone who lives in Panama, you might do the following:

package main

import (
    "fmt"
    "time"
)

func retrieveNormalizedTimeFromDataSource() time.Time {
    // Normally you'd fetch the time from a specific datasource here.
    return time.Date(2016, time.April, 26, 6, 10, 0, 0, time.UTC)
}

func main() {
    // Make sure to handle the error ;), omitted for brevity
    userLocation, _ := time.LoadLocation("America/Panama")
    t := retrieveNormalizedTimeFromDataSource()
    t = t.In(userLocation)
    fmt.Println(t.Format(time.RFC822Z))
}

// Output: 26 Apr 16 01:10 -0500

time.Date is another way to construct a time.Time struct, more about that here.

You can also define your own zones, be wary though, you’ll have to keep track of things like Daylight Saving Time yourself. The LoadLocation way of handling this problem is preferred.

This is how you would define your own zone:

package main

import (
    "fmt"
    "time"
)

func retrieveNormalizedTimeFromDataSource() time.Time {
    // Normally you'd fetch the time from a specific datasource here.
    return time.Date(2016, time.April, 26, 6, 10, 0, 0, time.UTC)
}

func main() {
    userLocation := time.FixedZone("example", 3600*8) // UTC +0800
    t := retrieveNormalizedTimeFromDataSource()
    t = t.In(userLocation)
    fmt.Println(t.Format(time.RFC822Z))
}

// Output: 26 Apr 16 14:10 +0800

The offset, the second parameter, is amount of seconds east of UTC. Which means you should use a negative amount if you want to go west of UTC.

Parsing

Parsing is pretty much the inverse of formatting, and uses the same reference time. Let’s say we want to parse the date 26-04-2016 08:10:23.

package main

import (
    "fmt"
    "time"
)

func main() {
    t, _ := time.Parse("02-01-2006 15:04:05", "26-04-2016 08:10:23")
    fmt.Println(t.Format(time.RFC822Z))
}

// Output: 26 Apr 16 08:10 +0000

As you can see, my used input 26-04-2016 08:10:23 is parsed based on the reference time 02-01-2006 15:04:05.

My machine’s time offset is UTC+0200, however the date has been parsed as UTC+0000. If you’re not careful this may cause unexpected results.

There are two ways to fix this, first of all, you could provide an offset, like this:

package main

import (
    "fmt"
    "time"
)

func main() {
    t, _ := time.Parse("02-01-2006 15:04:05 -0700", "26-04-2016 08:10:23 +0200")
    fmt.Println(t.Format(time.RFC822Z))
}

// Output: 26 Apr 16 08:10 +0200

But ParseInLocation might be easier, depending on your scenario. It works the same as Parse, except you specify a Location as second parameter.

package main

import (
    "fmt"
    "time"
)

func main() {
    t, _ := time.ParseInLocation("02-01-2006 15:04:05", "26-04-2016 08:10:23", time.Local)
    fmt.Println(t.Format(time.RFC822Z))
}

// Output: 26 Apr 16 08:10 +0200

That’s pretty much it for parsing :)!

Comparison & calculations

Using time for comparisons and calculations is the last subject I would like to cover. As an example I will show you how to compare two dates:

package main

import (
    "fmt"
    "time"
)

func main() {
    // Tiny disclaimer: All I could find about the first C release was the
    // year, so May 15 is fiction ;) Just like the time for both of them :)
    cReleaseDate := time.Date(1972, time.May, 15, 12, 0, 0, 0, time.UTC)
    goReleaseDate := time.Date(2009, time.November, 10, 12, 0, 0, 0, time.UTC)

    if goReleaseDate.After(cReleaseDate) {
        fmt.Println("Go was released years after C!")
    } else {
        fmt.Println("C was inspired by Go. Go was first.")
    }
}

// Output: Go was released years after C!

A small reminder on using Before and After. You would use it just as you would read it in English: if goReleaseDate came After cReleaseDate.

Other than that, it’s possible to calculate the difference between two dates:

package main

import (
    "fmt"
    "time"
)

func main() {
    blinkOne := time.Date(2016, time.April, 26, 8, 10, 23, int(time.Millisecond*500), time.UTC)
    blinkTwo := time.Date(2016, time.April, 26, 8, 10, 27, int(time.Millisecond*250), time.UTC)
    timeBetweenBlinks := blinkTwo.Sub(blinkOne)
    fmt.Printf("%s between two blinks", timeBetweenBlinks)
}

// Output: 3.75s between two blinks

Depending on your usecase there is a shorthand way to do this as well.

package main

import (
    "fmt"
    "time"
)

func main() {
    start := time.Now()
    for i := 0; i < 1000; i++ {
    }
    duration := time.Since(start)
    fmt.Printf("Operation took: %s", duration)
}

// Output: Operation took: 1.808µs

Conclusion

I hope this was a decent introduction to the time package if you’re starting out with Go. Otherwise I hope it serves as good reference material when you’re dealing with the time package :).

Hope you enjoyed the article!


Get Machiel Mail in your inbox!

Of course I have a newsletter, all the cool kids have them! Sign up if you want to get notified of the occasional blog post I write. They will be mostly about Go, or simply about projects I am working on. And it might just turn out that they are interesting!

And if you're wondering, will he send sp... No, I will never send you any spam!

comments powered by Disqus