how to control the timezone of time.Now()

3,667 views
Skip to first unread message

Andrei Matei

unread,
Oct 26, 2021, 2:58:16 PM10/26/21
to golan...@googlegroups.com, r...@golang.org, Radu Berinde
Hello Go friends,

I'm finding it difficult to satisfactorily control the timezone used by the time.Now() function. The Go team have taken positions in the past that seem to not make this easy; I'd like to see if I can gather sympathy for changes, or perhaps if there's some obvious idea that I'm missing.

In CockroachDB, we like all our timestamps to have the UTC timezone. This is important for printing them out similarly across a cluster of geographically-distributed processes, and also because time.Now() timestamps sometimes end up exposed as SQL values, where the time zone matters in various ways.

If you accept that you want all the time.Now() calls in the process to return UTC timestamps, the question is how to achieve that. We want a solution that is confined to our program; we don't want to rely on the environment having set the TZ envvar.

For the longest time, we used a utility function like:

func Now() time.Time {
  return time.Now().UTC()
}

and we had a linter disallowing direct use of time.Now(). The problem with this is that the UTC() call strips out the monotonic clock part. I don't fully understand the reason for this; the UTC() call seems to have been bucketed in with other calls like Truncate() which want to strip the mono part. So, we lose the benefits of the monotonic clock, and also the time.Since() calls become twice as expensive because an optimization is disabled when there's no mono.
Would it be feasible at all to get a flavor of UTC() that does not do the stripping?

If not, let's see what we can do to avoid the UTC() call. One possibility is os.Setenv("TZ", ""). The problem with that, I think, is that it needs to be done before initLocal() runs, which is not easy to control.

Another way to do it is by setting the time.Local global. I thought that the reason why this global variable exists is precisely for setting it in func init()'s, but then I got a bit disillusioned.
First I tried

func init() {
  time.Local = time.UTC
}

A problem with doing this is that you end up violating what appears to be a documented invariant of time.Time:

Setting time.Local = time.UTC results in time.Now() producing timestamps with local == &utcLoc. I don't know if violating that invariant actually has any consequences for the time library, but at the very least it breaks the round-tripping of UTC values through marshall/unmarshall (e.g. json), in the sense that reflect.DeepEqual() no longer considers the un-marhsalled value equal to the original. The difficulties with round-tripping time.Time have been reported several times and were not accepted; still, time.Now().UTC() used to round-trip fine - which was making our tests happy.

One weird trick that works is setting time.Local = nil. That does what I want, but seems to be straying even further from intended uses of the API.

Reading around, I see that the invariant violation above was rejected as a bug report in the past. More generally, I see that the whole idea of controlling the process-wide timezone was rejected (also here). And yet time.Local is exported (and mutable), so I do wonder why that is.

So, would anyone have another suggestion about how to enforce process-wide UTC timezones while preserving the monotonic clock readings?
Or, any chance for re-litigating introducing a SetLocal() function?

Thanks!

- Andrei

Ian Lance Taylor

unread,
Oct 26, 2021, 3:15:37 PM10/26/21
to Andrei Matei, golang-nuts, Russ Cox, Radu Berinde
As you know, the timezone only affects the presentation of the
time.Time value, which is when a time.Time turns into something else,
such as a string. So I would approach this problem either by 1)
consistently running the program with the TZ environment variable set,
or 2) consistently controlling the places where a time.Time turns into
something else, which is usually a relatively small number of places.
I don't know that that helps much, but that is my take on the issue.

Ian

Andrei Matei

unread,
Oct 26, 2021, 5:08:08 PM10/26/21
to Ian Lance Taylor, golang-nuts, Russ Cox, Radu Berinde
If you accept that there are reasons for setting TZ for a particular program, then 
would you also agree that there are reasons for wanting to do the same from inside
the program? We distribute our software for others to run, so we don't generally have 
control / would rather not rely on the environment.

Alternatively, if we can get (a flavor of) Time.UTC() to not strip the mono any more, 
I'd also be happy with that.

- Andrei
 
or 2) consistently controlling the places where a time.Time turns into
something else, which is usually a relatively small number of places.
 I don't know that that helps much, but that is my take on the issue.

Ian

Ian Lance Taylor

unread,
Oct 26, 2021, 6:10:09 PM10/26/21
to Andrei Matei, golang-nuts, Russ Cox, Radu Berinde
On Tue, Oct 26, 2021 at 2:07 PM Andrei Matei <and...@cockroachlabs.com> wrote:
>
> If you accept that there are reasons for setting TZ for a particular program, then
> would you also agree that there are reasons for wanting to do the same from inside
> the program? We distribute our software for others to run, so we don't generally have
> control / would rather not rely on the environment.
>
> Alternatively, if we can get (a flavor of) Time.UTC() to not strip the mono any more,
> I'd also be happy with that.

It's not obvious to me how common it is for a program to require that
all times be in UTC, but be unable to control that at the point where
the time.Time value is converted to some other type. If this is
common, then perhaps Go should provide some solution. But is it
common?

There is already a mechanism for setting TZ from inside the program,
but as you've observed there is no mechanism that will ensure that
that happens before the possibility that package initialization
fetches the current time zone. It seems unlikely that Go would add
this feature, given that it's easy to use a tiny wrapper program to
set TZ and then invoke the real program.

Personally I think that a version of UTC that doesn't strip the
monotonic time is solving the problem at the wrong end. The end that
matters is the conversion out of time.Time, not the creation of a
time.Time.

Ian

peterGo

unread,
Oct 26, 2021, 6:14:02 PM10/26/21
to golang-nuts
Andrei,

For database times enforce the use of these convenience functions:

func DBNow() time.Time {
    return DBTime(time.Now())
}

func DBTime(t time.Time) time.Time {
    return t.UTC()
}

Peter

Andrei Matei

unread,
Oct 27, 2021, 6:38:17 PM10/27/21
to Ian Lance Taylor, golang-nuts, Russ Cox, Radu Berinde
Fair enough.
At least for my edification, though, I would benefit from a commentary 
about why exactly it is that UTC() strips the monos, if you happen to 
understand it. The only thing comment on the topic I found is here, from Russ:
He said:
> I should check on In, Local, UTC too. The argument for stripping in
> those cases is that you're in some sense explicitly asking to work in wall times.

I don't quite see that argument. Calling UTC() is not "explicitly asking to work in wall times". 
At least, that's not exactly the interpretation in our codebase. I would argue for
a more limited interpretation: the call to UTC() should do something for the contexts
in which a timezone is relevant, but I don't quite see why it does something 
(detrimental) for contexts where it's not relevant (i.e. time.Since()).

Thanks!

- Andrei

 

Ian

Ian Lance Taylor

unread,
Oct 27, 2021, 7:01:58 PM10/27/21
to Andrei Matei, golang-nuts, Russ Cox, Radu Berinde
On Wed, Oct 27, 2021 at 3:37 PM Andrei Matei <and...@cockroachlabs.com> wrote:
>
> At least for my edification, though, I would benefit from a commentary
> about why exactly it is that UTC() strips the monos, if you happen to
> understand it. The only thing comment on the topic I found is here, from Russ:
> He said:
> > I should check on In, Local, UTC too. The argument for stripping in
> > those cases is that you're in some sense explicitly asking to work in wall times.
>
> I don't quite see that argument. Calling UTC() is not "explicitly asking to work in wall times".
> At least, that's not exactly the interpretation in our codebase. I would argue for
> a more limited interpretation: the call to UTC() should do something for the contexts
> in which a timezone is relevant, but I don't quite see why it does something
> (detrimental) for contexts where it's not relevant (i.e. time.Since()).

A time.Time value always represents an instant in time to nanosecond
precision, and the specific instant that it represents is independent
of the timezone. The only effect of changing the timezone is that it
changes the way that the time.Time converts to other representations,
via methods like String or Format or Date or Clock. The argument here
is that a program would only call the UTC method if it wants to change
the way that those conversions work, so it presumably cares about the
wall time, not monotonic time. I understand that that is not true in
your case. The argument doesn't always hold. But it often does.

Ian

Radu Berinde

unread,
Oct 27, 2021, 7:28:37 PM10/27/21
to Ian Lance Taylor, Andrei Matei, golang-nuts, Russ Cox
Hi Ian,

> The argument here
is that a program would only call the UTC method if it wants to change
the way that those conversions work, so it presumably cares about the
wall time, not monotonic time.

But in those contexts, the monotonic time wouldn't make a difference - so what is the upside of stripping it? Are there cases I'm not seeing where the mono time would interfere with these "standard" usage patterns?

Thanks,
-Radu

Ian Lance Taylor

unread,
Oct 27, 2021, 10:05:00 PM10/27/21
to Radu Berinde, Andrei Matei, golang-nuts, Russ Cox
On Wed, Oct 27, 2021 at 4:28 PM Radu Berinde <ra...@cockroachlabs.com> wrote:
>
> > The argument here
> is that a program would only call the UTC method if it wants to change
> the way that those conversions work, so it presumably cares about the
> wall time, not monotonic time.
>
> But in those contexts, the monotonic time wouldn't make a difference - so what is the upside of stripping it? Are there cases I'm not seeing where the mono time would interfere with these "standard" usage patterns?

That's true, keeping the monotonic time wouldn't make much difference.
Stripping it gives you consistent behavior with things like time.Sub,
but I agree that it's not a major point. But I don't think we can
really change the behavior now.

Ian

Uli Kunitz

unread,
Oct 28, 2021, 2:57:49 AM10/28/21
to golang-nuts
I believe time.Local = time.UTC is the right approach. It is true that the loc field of the time structure will be set to &UTC and not zero, but I couldn't find a path where this should generate a problem. From my point of view I would regard any issue resulting from it as a bug.

Uli

Axel Wagner

unread,
Oct 28, 2021, 3:14:21 AM10/28/21
to golang-nuts
FWIW, based on a quick test, setting TZ="" also leads to a non-nil loc field:
So, `time.Local = time.UTC` at least shouldn't lead to more adverse effects than setting TZ="".

--
You received this message because you are subscribed to the Google Groups "golang-nuts" group.
To unsubscribe from this group and stop receiving emails from it, send an email to golang-nuts...@googlegroups.com.
To view this discussion on the web visit https://groups.google.com/d/msgid/golang-nuts/e489b1b5-e42c-4a1e-aa91-4ffa644e5b48n%40googlegroups.com.
Reply all
Reply to author
Forward
0 new messages