Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

time: Location() reports UTC inconsistently #48493

Closed
dallbee opened this issue Sep 20, 2021 · 5 comments
Closed

time: Location() reports UTC inconsistently #48493

dallbee opened this issue Sep 20, 2021 · 5 comments
Labels
FrozenDueToAge NeedsInvestigation Someone must examine and confirm this is a valid issue and not a duplicate of an existing one.
Milestone

Comments

@dallbee
Copy link

dallbee commented Sep 20, 2021

The Location() method on a Time struct reports UTC as "local" when acquired from /etc/localtime, but "UTC" when acquired from the TZ environment variable.

This seems related to:
#45960

However, this inconsistency is produced without any parse or marshal methods.

What version of Go are you using (go version)?

$ go version
go version go1.17.1 linux/amd64

Does this issue reproduce with the latest release?

Yes

What operating system and processor architecture are you using (go env)?

go env Output
$ go env
GO111MODULE="on"
GOARCH="amd64"
GOBIN=""
GOCACHE="/home/dylan/.cache/go-build"
GOENV="/home/dylan/.config/go/env"
GOEXE=""
GOEXPERIMENT=""
GOFLAGS=""
GOHOSTARCH="amd64"
GOHOSTOS="linux"
GOINSECURE=""
GOMODCACHE="/home/dylan/go/pkg/mod"
GOOS="linux"
GOPATH="/home/dylan/go"
GOPROXY="https://proxy.golang.org,direct"
GOROOT="/usr/lib/go"
GOSUMDB="sum.golang.org"
GOTMPDIR=""
GOTOOLDIR="/usr/lib/go/pkg/tool/linux_amd64"
GOVCS=""
GOVERSION="go1.17.1"
GCCGO="gccgo"
AR="ar"
CC="gcc"
CXX="g++"
CGO_ENABLED="1"
GOMOD="/dev/null"
CGO_CFLAGS="-g -O2"
CGO_CPPFLAGS=""
CGO_CXXFLAGS="-g -O2"
CGO_FFLAGS="-g -O2"
CGO_LDFLAGS="-g -O2"
PKG_CONFIG="pkg-config"
GOGCCFLAGS="-fPIC -m64 -pthread -fmessage-length=0 -fdebug-prefix-map=/tmp/go-build817213888=/tmp/go-build -gno-record-gcc-switches"

What did you do?

Using the following program (playground link):

func main() {
    x := time.Now()
    fmt.Println(x.Location())
}
# with localtime set to UTC
$ doas ln -sf /usr/share/zoneinfo/UTC /etc/localtime

# load from /etc/localtime by unsetting TZ
$ env -u TZ go run main.go
Local

# load from TZ
$ env TZ="UTC" go run main.go
UTC
$ env TZ="" go run main.go
UTC

What did you expect to see?

Consistent location behavior between environment variable and /etc/localtime methods of setting time.

What did you see instead?

The output of Location() changes based on where it obtained the timezone information.

Additional Notes

The internal documentation on the Time struct's loc variable is incorrect:

// All UTC times are represented with loc==nil, never loc==&utcLoc.

After String(), the loc field becomes non-nil, and is set to UTC:
https://play.golang.org/p/rqgA876kFIW

These are just internal docs, so I'm not suggesting this itself is a bug. The bug report is about the Location behavior noted in prior sections.

@dr2chase dr2chase added the NeedsInvestigation Someone must examine and confirm this is a valid issue and not a duplicate of an existing one. label Sep 20, 2021
@dr2chase
Copy link
Contributor

@rsc
I don't know if this is a bug or WAI, but it is nicely described.

@ianlancetaylor
Copy link
Contributor

If TZ is not set, the location is always set to Local. That seems reasonable and consistent. We don't want to try to figure out exactly which timezone corresponds to /etc/localtime. That is true even in the particular case that /etc/localtime happens to be UTC.

If TZ is set to a non-empty string, then that is used to determine the location. That also seems reasonable.

I think the only question here is how to handle TZ being set to the empty string. Right now that is the same as TZ being set to UTC. That is consistent with the C library behavior.

@ALTree ALTree added this to the Unplanned milestone Sep 21, 2021
@dallbee
Copy link
Author

dallbee commented Sep 21, 2021

@ianlancetaylor thanks for you thoughts. Agreed on your 2nd and 3rd points.

We don't want to try to figure out exactly which timezone corresponds to /etc/localtime. That is true even in the particular case that /etc/localtime happens to be UTC

Respectfully, why not? Go already reads /etc/localtime in this case, so there's not any syscall being avoided.

For comparison, in C:

#include <stdio.h>
#include <time.h>

int main () {
   time_t rawtime;
   struct tm *info;
   char buffer[80];

   time(&rawtime);
   info = localtime(&rawtime);

   strftime(buffer,80,"%Z", info);
   printf("Timezone: %s\n", buffer);
}
$ doas ln -sf /usr/share/zoneinfo/UTC /etc/localtime
$ env -u TZ ./a.out
Timezone: UTC
$ env TZ="" ./a.out
Timezone: UTC
$ env TZ="UTC" ./a.out
Timezone: UTC

# strace of reads to /etc/localtime
$ env -u TZ strace ./a.out 2>&1 | grep localtime
openat(AT_FDCWD, "/etc/localtime", O_RDONLY|O_CLOEXEC) = 3
$ env TZ="" strace ./a.out 2>&1 | grep localtime
$ env TZ="UTC" strace ./a.out 2>&1 | grep localtime

@ianlancetaylor
Copy link
Contributor

Respectfully, why not? Go already reads /etc/localtime in this case, so there's not any syscall being avoided.

It's a fair question. My only answer is that is how the time package has worked since the current version was introduced in https://golang.org/cl/5392041 in 2011. It means that time.LoadLocation("Local") gives you a timezone whose name is Local that represents the local time zone. It lets us do the right thing on systems like Windows and Plan 9 that don't require the IANA database.

Changing this now would have a decent chance of breaking working Go programs. I think that any such change would have to through the proposal process (https://golang.org/s/proposal-process).

@dallbee
Copy link
Author

dallbee commented Sep 21, 2021

Changing this now would have a decent chance of breaking working Go programs. I think that any such change would have to through the proposal process (https://golang.org/s/proposal-process).

Sensible. I brought it up because it leads to some interesting scenarios, such as reflect.DeepEqual not considering the time from time.Now() and a parsed equivalent to be equal even if they've had their clocks rounded - the timezone can still differ. Of course, the answer is to use Equal, so it's a minor point.

As for a proposal, I don't think there's a way to change this in a backward compatible manner. I'm not even sure I'd suggest any changes to the existing documentation.

Perhaps, for this one, this closed issue thread will suffice. A future soul might save a few minutes when they're investigating the oddity.

Glad to know it's at least working as designed, Thanks!

@golang golang locked and limited conversation to collaborators Sep 21, 2022
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
FrozenDueToAge NeedsInvestigation Someone must examine and confirm this is a valid issue and not a duplicate of an existing one.
Projects
None yet
Development

No branches or pull requests

5 participants