Local date time calculations in Go

Posted on Thu 22 March 2018

For a side project I'm writing in Go, I needed to send an email to each registered user during the night. Their night actually, according to each user's time zone. More precisely at a random time between 3am and 4am to spread email sending and avoid any rate limiting from SMTP providers or spam detection systems.

Coming from a Java/Scala background, I was looking for a large number of setters or offsetting methods on the time type. Here's what it would look like in Scala using classes in java.time:

val tz = user.timeZone // e.g. "Europe/Paris"

// the current wall clock time in the user's time zone
val userTime = ZonedDateTime.now(ZoneId.of(tz))

// tomorrow sometime between 3am and 4am
val emailSendTime = userTime
    .plusDays(1)
    .withHour(3)
    .plusSeconds(random.nextInt(3600))

// the instant (absolute timestamp) to be used by the task scheduler
val emailInstant = emailLocalTime.toInstant

Go puts simplicity first (too much IMHO but that's another discussion) and has only one type for time: time.Time. No wall clock time like Java's LocalDateTime or ZonedDateTime and no absolute Instant.

Similarly, time.Time has no setter method and only one offsetting method, AddDate(years, months, days). So with these in hand, how do we implement the above calculation?

Go has a couple of tricks here:

  • time.Time always has an associated time zone that by default is UTC.
  • time.Time has two destructuring functions, Time.Date() and Time.Clock that return the date and clock's constituents.
  • there is a Date(year, month, day, hour, min, sec, nsec, location) constructor function.

With that in hand, we can write the Go version:

location, _ := time.LoadLocation(user.TimeZone) // e.g. "Europe/Paris"

// the current wall clock time in the user's time zone
userTime := time.Now().In(location)

// destructure the time's date
year, month, day := userTime.Date()

emailSendTime := time.Date(
    year, month, day+1,        // date
    3, 0, rand.Intn(3600), 0,  // time (with nanosec)
    userTime.Location())

(yes, I skipped the dreaded if err != nil after LoadLocation)

An interesting thing to note regarding the Date function, is that we can pass out of range values for all parameters. For example, passing 18 for the month will set it to 6 and increment the year by one.

Nothing complicated here, but I had to "unlearn" what I knew from Java to find that a couple of simple functions were all that was needed to perform arbitrary time calculations. Now I need to look at how this works in Rust :-)



Micro benchmarks can make you short-sighted

Go: the Good, the Bad and the Ugly