Using Time, Timezones and Location in Go

Aug 8, 2013


I ran into a problem today. I was building code to consume NOAA’s tide station XML document and quickly realized I was in trouble. Here is a small piece of that XML document:

<timezone>LST/LDT</timezone>
<item>
<date>2013/01/01</date>
<day>Tue</day>
<time>02:06 AM</time>
<predictions_in_ft>19.7</predictions_in_ft>
<predictions_in_cm>600</predictions_in_cm>
<highlow>H</highlow>
</item>

If you notice the timezone tag, it states the time is in Local Standard Time / Local Daylight Time. This is a real problem because I need to store this data in UTC. Without a proper timezone I am lost. After scratching my head for a bit my business partner showed me two API’s that take a latitude and longitude position and return timezone information. Luckily for me I have a latitude and longitude position for each tide station.

If you open this web page you can read the documentation for Google’s Timezone API:

https://developers.google.com/maps/documentation/timezone/

The API is fairly simple. It requires a location, timestamps and a flag to identify if the requesting application is using a sensor, like a GPS device, to determine the location.

Here is a sample call to the Google API and response:

https://maps.googleapis.com/maps/api/timezone/json?location=38.85682,-92.991714&sensor=false&timestamp=1331766000

{
    "dstOffset" : 3600.0,
    "rawOffset" : -21600.0,
    "status" : "OK",
    "timeZoneId" : "America/Chicago",
    "timeZoneName" : "Central Daylight Time"
}

There is a limit of 2,500 calls a day. For my initial load of the tide stations, I knew I was going to hit that limit and I didn’t want to wait several days to load all the data. So my business partner found the timezone API from GeoNames.

If you open this web page you can read the documentation for GeoNames’s Timezone API:

http://www.geonames.org/export/web-services.html#timezone

The API requires a free account which is real quick to setup. Once you activate your account you need to find the account page and activate your username for use with the API.

Here is a sample call to the GeoNames API and response:

http://api.geonames.org/timezoneJSON?lat=47.01&lng=10.2&username=demo

{
    "time":"2013-08-09 00:54",
    "countryName":"Austria",
    "sunset":"2013-08-09 20:40",
    "rawOffset":1,
    "dstOffset":2,
    "countryCode":"AT",
    "gmtOffset":1,
    "lng":10.2,
    "sunrise":"2013-08-09 06:07",
    "timezoneId":"Europe/Vienna",
    "lat":47.01
}

This API returns a bit more information. There is no limit to the number of calls you can make but the response times are not guaranteed. I used it for several thousand calls and had no problems.

So now we have two different web calls we can use to get the timezone information. Let’s look at how we can use Go to make the Google web call and get an object back that we can use in our program.

First, we need to define a new type that can contain the information we will get back from the API.

package main

import (
    "encoding/json"
    "fmt"
    "io/ioutil"
    "net/http"
    "time"
)

const googleURI = "https://maps.googleapis.com/maps/api/timezone/json?location=%f,%f&timestamp=%d&sensor=false"

type GoogleTimezone struct {
    DstOffset    float64 bson:&quot;dstOffset&quot;
    RawOffset    float64 bson:&quot;rawOffset&quot;
    Status       string  bson:&quot;status&quot;
    TimezoneID   string  bson:&quot;timeZoneId&quot;
    TimezoneName string  bson:&quot;timeZoneName&quot;
}

Go has awesome support for JSON and XML. If you look at the GoogleTimezone struct you will see that each field contains a "tag". A tag is extra data attached to each field that can later be retrieved by our program using reflection. To learn more about tags read this document:

http://golang.org/pkg/reflect/#StructTag

The encoding/json package has defined a set of tags it looks for to help with marshaling and unmarshaling JSON data. To learn more about the JSON support in Go read these documents:

http://golang.org/doc/articles/json_and_go.html
http://golang.org/pkg/encoding/json/

If you make the field names in your struct the same as the field names in the JSON document, you don’t need to use the tags. I didn’t do that so the tags are there to tell the Unmarshal function how to map the data.

Let’s look at a function that can make the API call to Google and unmarshal the JSON document to our new type:

func RetrieveGoogleTimezone(latitude float64, longitude float64) (googleTimezone *GoogleTimezone, err error) {
    defer func() {
        if r := recover(); r != nil {
            err = fmt.Errorf("%v", r)
        }
    }()

    uri := fmt.Sprintf(googleURI, latitude, longitude, time.Now().UTC().Unix())

    resp, err := http.Get(uri)
    if err != nil {
        return googleTimezone, err
    }

    defer resp.Body.Close()

    // Convert the response to a byte array
    rawDocument, err = ioutil.ReadAll(resp.Body)
    if err != nil {
        return googleTimezone, err
    }

    // Unmarshal the response to a GoogleTimezone object
    googleTimezone = new(GoogleTimezone)
    if err = json.Unmarshal(rawDocument, googleTimezone); err != nil {
        return googleTimezone, err
    }

    if googleTimezone.Status != "OK" {
        err = fmt.Errorf("Error : Google Status : %s", googleTimezone.Status)
        return googleTimezone, err
    }

    if len(googleTimezone.TimezoneId) == 0 {
        err = fmt.Errorf("Error : No Timezone Id Provided")
        return googleTimezone, err
    }

    return googleTimezone, err
}

The web call and error handling is fairly boilerplate so let’s just talk briefly about the Unmarshal call.

    rawDocument, err = ioutil.ReadAll(resp.Body)

    err = json.Unmarshal(rawDocument, googleTimezone)

When the web call returns, we take the response and store it in a byte array. Then we call the json Unmarshal function, passing the byte array and a reference to our return type pointer variable. The Unmarshal call creates an object of type GoogleTimezone, extracts and copies the data from the returned JSON document and sets the value of our pointer variable. It’s really brilliant. If any fields can’t be mapped they are simply ignored. The Unmarshal call will return an error if there are casting issues.

So this is great, we can get the timezone data and unmarshal it to an object with three lines of code. Now the only problem is, how the heck do we use the timezoneid to set our location?

Here is the problem again. We have to take the local time from the feed document, apply the timezone information and then convert everything UTC.

Let’s look at the feed document again:

<timezone>LST/LDT</timezone>
<item>
<date>2013/01/01</date>
<day>Tue</day>
<time>02:06 AM</time>
<predictions_in_ft>19.7</predictions_in_ft>
<predictions_in_cm>600</predictions_in_cm>
<highlow>H</highlow>
</item>

Assuming we have extracted the data from this document, how can we use the timezoneid to get us out of this jam? Look at the code I wrote in the main function. It uses the time.LoadLocation function and the timezone id we get from the API call to solve the problem:

func main() {
    // Call to get the timezone for this lat and lng position
    googleTimezone, err := RetrieveGoogleTimezone(38.85682, -92.991714)
    if err != nil {
        fmt.Printf("ERROR : %s", err)
        return
    }

    // Pretend this is the date and time we extracted
    year := 2013
    month := 1
    day := 1
    hour := 2
    minute := 6

    // Capture the location based on the timezone id from Google
    location, err := time.LoadLocation(googleTimezone.TimezoneId)
    if err != nil {
        fmt.Printf("ERROR : %s", err)
        return
    }

    // Capture the local and UTC time based on timezone
    localTime := time.Date(year, time.Month(month), day, hour, minute, 0, 0, location)
    utcTime := localTime.UTC()

    // Display the results
    fmt.Printf("Timezone:\t%s\n", googleTimezone.TimezoneId)
    fmt.Printf("Local Time: %v\n", localTime)
    fmt.Printf("UTC Time: %v\n", utcTime)
}

Here is the output:

Timezone:   America/Chicago
Local Time: 2013-01-01 02:06:00 -0600 CST
Time:       2013-01-01 08:06:00 +0000 UTC

Everything worked like a champ. Our localTime variable is set to CST or Central Standard Time, which is where Chicago is located.  The Google API provided the correct timezone for the latitude and longitude because that location falls within Missouri.

https://maps.google.com/maps?q=39.232253,-92.991714&z=6

The last question we have to ask is how did the LoadLocation function take that timezone id string and make this work. The timezone id contains both a country and city (America/Chicago). There must be thousands of these timezone ids.

If we take a look at the time package documentation for LoadLocation, we will find the answer:

http://golang.org/pkg/time/#LoadLocation

Here is the documentation for LoadLocation:

LoadLocation returns the Location with the given name.

If the name is "" or "UTC", LoadLocation returns UTC. If the name is "Local", LoadLocation returns Local.

Otherwise, the name is taken to be a location name corresponding to a file in the IANA Time Zone database, such as "America/New_York".

The time zone database needed by LoadLocation may not be present on all systems, especially non-Unix systems. LoadLocation looks in the directory or uncompressed zip file named by the ZONEINFO environment variable, if any, then looks in known installation locations on Unix systems, and finally looks in $GOROOT/lib/time/zoneinfo.zip.

If you read the last paragraph you will see that the LoadLocation function is reading a database file to get the information. I didn’t download any database, nor did I set an environment variable called ZONEINFO. The only answer is that this zoneinfo.zip file exists in GOROOT. Let’s take a look:

Sure enough there is a zoneinfo.zip file located in the lib/time directory where Go was installed. Very Cool !!

There you have it. Now you know how to use the time.LoadLocation function to help make sure your time values are always in the correct timezone. If you have a latitude and longitude, you can use either API to get that timezone id.

I have added a new package called timezone to the GoingGo repository in Github if you want a reusable copy of the code with both API calls.  Here is the entire working sample program:

package main

import (
    "encoding/json"
    "fmt"
    "io/ioutil"
    "net/http"
    "time"
)

const (
    googleURI = "https://maps.googleapis.com/maps/api/timezone/json?location=%f,%f&timestamp=%d&sensor=false"
)

type GoogleTimezone struct {
    DstOffset    float64 bson:&quot;dstOffset&quot;
    RawOffset    float64 bson:&quot;rawOffset&quot;
    Status       string  bson:&quot;status&quot;
    TimezoneID   string  bson:&quot;timeZoneId&quot;
    TimezoneName string  bson:&quot;timeZoneName&quot;
}

func main() {
    // Call to get the timezone for this lat and lng position
    googleTimezone, err := RetrieveGoogleTimezone(38.85682, -92.991714)
    if err != nil {
        fmt.Printf("ERROR : %s", err)
        return
    }

    // Pretend this is the date and time we extracted
    year := 2013
    month := 1
    day := 1
    hour := 2
    minute := 6

    // Capture the location based on the timezone id from Google
    location, err := time.LoadLocation(googleTimezone.TimezoneID)
    if err != nil {
        fmt.Printf("ERROR : %s", err)
        return
    }

    // Capture the local and UTC time based on timezone
    localTime := time.Date(year, time.Month(month), day, hour, minute, 0, 0, location)
    utcTime := localTime.UTC()

    // Display the results
    fmt.Printf("Timezone:\t%s\n", googleTimezone.TimezoneID)
    fmt.Printf("Local Time: %v\n", localTime)
    fmt.Printf("UTC Time: %v\n", utcTime)
}

func RetrieveGoogleTimezone(latitude float64, longitude float64) (googleTimezone *GoogleTimezone, err error) {
    defer func() {
        if r := recover(); r != nil {
            err = fmt.Errorf("%v", r)
        }
    }()

    uri := fmt.Sprintf(googleURI, latitude, longitude, time.Now().UTC().Unix())

    resp, err := http.Get(uri)
    if err != nil {
        return googleTimezone, err
    }

    defer resp.Body.Close()

    // Convert the response to a byte array
    rawDocument, err := ioutil.ReadAll(resp.Body)
    if err != nil {
        return googleTimezone, err
    }

    // Unmarshal the response to a GoogleTimezone object
    googleTimezone = new(GoogleTimezone)
    if err = json.Unmarshal(rawDocument, &googleTimezone); err != nil {
        return googleTimezone, err
    }

    if googleTimezone.Status != "OK" {
        err = fmt.Errorf("Error : Google Status : %s", googleTimezone.Status)
        return googleTimezone, err
    }

    if len(googleTimezone.TimezoneID) == 0 {
        err = fmt.Errorf("Error : No Timezone Id Provided")
        return googleTimezone, err
    }

    return googleTimezone, err
}


Go Training

We have taught Go to thousands of developers all around the world since 2014. There is no other company that has been doing it longer and our material has proven to help jump start developers 6 to 12 months ahead of their knowledge of Go. We know what knowledge developers need in order to be productive and efficient when writing software in Go.

Our Go, Web and Data Science classes are perfect for both experienced and beginning engineers. We start every class from the beginning and get very detailed about the internals, mechanics, specification, guidelines, best practices and design philosophies. We cover a lot about "if performance matters" with a focus on mechanical sympathy, data oriented design, decoupling and writing production software.

Learn More

To learn about Corporate training events, options and special pricing please contact:

William Kennedy
ArdanLabs (www.ardanlabs.com)
bill@ardanlabs.com