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

$ go version
go version go1.24.4 darwin/arm64

Does this issue reproduce with the latest release?

yes, go 1.24.4 is latest.

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

go env Output
$ go env
AR='ar'
CC='clang'
CGO_CFLAGS='-O2 -g'
CGO_CPPFLAGS=''
CGO_CXXFLAGS='-O2 -g'
CGO_ENABLED='1'
CGO_FFLAGS='-O2 -g'
CGO_LDFLAGS='-O2 -g'
CXX='clang++'
GCCGO='gccgo'
GO111MODULE=''
GOARCH='arm64'
GOARM64='v8.0'
GOAUTH='netrc'
GOBIN=''
GOCACHE='/Users//Library/Caches/go-build'
GOCACHEPROG=''
GODEBUG=''
GOENV='/Users/myuser/Library/Application Support/go/env'
GOEXE=''
GOEXPERIMENT=''
GOFIPS140='off'
GOFLAGS=''
GOGCCFLAGS='-fPIC -arch arm64 -pthread -fno-caret-diagnostics -Qunused-arguments -fmessage-length=0 -ffile-prefix-map=/var/folders/j7/py1yk76x32sb49vv6cdltx2m0000gn/T/go-build1644786394=/tmp/go-build -gno-record-gcc-switches -fno-common'
GOHOSTARCH='arm64'
GOHOSTOS='darwin'
GOINSECURE=''
GOMOD='/Users/myuser/git/test-race/go.mod'
GOMODCACHE='/Users/myuser/.gvm/pkgsets/go1.24.4/global/pkg/mod'
GONOPROXY='github.com/hashicorp/*,github.com/dhiaayachi/*'
GONOSUMDB='github.com/hashicorp/*,github.com/dhiaayachi/*'
GOOS='darwin'
GOPATH='/Users/myuser/.gvm/pkgsets/go1.24.4/global'
GOPRIVATE='github.com/hashicorp/*,github.com/dhiaayachi/*'
GOPROXY='https://proxy.golang.org,direct'
GOROOT='/Users/myuser/.gvm/gos/go1.24.4'
GOSUMDB='sum.golang.org'
GOTELEMETRY='local'
GOTELEMETRYDIR='/Users/myuser/Library/Application Support/go/telemetry'
GOTMPDIR=''
GOTOOLCHAIN='auto'
GOTOOLDIR='/Users/myuser/.gvm/gos/go1.24.4/pkg/tool/darwin_arm64'
GOVCS=''
GOVERSION='go1.24.4'
GOWORK=''
PKG_CONFIG='pkg-config'

What did you do?

Running this program with the race detector activated (go run -race main.go) result on a data race being detected. I tried to remove the dependency to go-cmp but I was unsuccessfully able to reproduce it without it, that said the stack trace show that it can be reproduced without go-cmp given the right use of the reflect package.

Adding

    t1 := time.Now()
    fmt.Println(t1)

before starting the 2 go routine resolve the race condition.

What did you expect to see?

No data race detected

What did you see instead?

A data race detected with the following output:

the details of the data race are the following:

race Output

WARNING: DATA RACE
Write at 0x000102ecd2c8 by goroutine 8:
  time.initLocal()
      /Users/myuser/.gvm/gos/go1.23.7/src/time/zoneinfo_unix.go:41 +0x394
  sync.(*Once).doSlow()
      /Users/myuser/.gvm/gos/go1.23.7/src/sync/once.go:76 +0xac
  sync.(*Once).Do()
      /Users/myuser/.gvm/gos/go1.23.7/src/sync/once.go:67 +0x40
  time.(*Location).get()
      /Users/myuser/.gvm/gos/go1.23.7/src/time/zoneinfo.go:96 +0x70
  time.Time.locabs()
      /Users/myuser/.gvm/gos/go1.23.7/src/time/time.go:494 +0x74
  time.Time.appendFormat()
      /Users/myuser/.gvm/gos/go1.23.7/src/time/format.go:668 +0x58
  time.Time.AppendFormat()
      /Users/myuser/.gvm/gos/go1.23.7/src/time/format.go:662 +0xf8
  time.Time.Format()
      /Users/myuser/.gvm/gos/go1.23.7/src/time/format.go:648 +0xa4
  time.Time.String()
      /Users/myuser/.gvm/gos/go1.23.7/src/time/format.go:546 +0x4c
  time.(*Time).String()
      :1 +0x4c
  github.com/google/go-cmp/cmp.formatOptions.FormatValue.func2()
      /Users/myuser/.gvm/pkgsets/go1.23.7/global/pkg/mod/github.com/google/go-cmp@v0.7.0/cmp/report_reflect.go:144 +0x110
  github.com/google/go-cmp/cmp.formatOptions.FormatValue()
      /Users/myuser/.gvm/pkgsets/go1.23.7/global/pkg/mod/github.com/google/go-cmp@v0.7.0/cmp/report_reflect.go:147 +0x31c
  github.com/google/go-cmp/cmp.formatOptions.FormatDiff()
      /Users/myuser/.gvm/pkgsets/go1.23.7/global/pkg/mod/github.com/google/go-cmp@v0.7.0/cmp/report_compare.go:125 +0x598
  github.com/google/go-cmp/cmp.formatOptions.formatDiffList()
      /Users/myuser/.gvm/pkgsets/go1.23.7/global/pkg/mod/github.com/google/go-cmp@v0.7.0/cmp/report_compare.go:315 +0x2350
  github.com/google/go-cmp/cmp.formatOptions.FormatDiff()
      /Users/myuser/.gvm/pkgsets/go1.23.7/global/pkg/mod/github.com/google/go-cmp@v0.7.0/cmp/report_compare.go:177 +0x159c
  github.com/google/go-cmp/cmp.formatOptions.FormatDiff()
      /Users/myuser/.gvm/pkgsets/go1.23.7/global/pkg/mod/github.com/google/go-cmp@v0.7.0/cmp/report_compare.go:192 +0x1770
  github.com/google/go-cmp/cmp.(*defaultReporter).String()
      /Users/myuser/.gvm/pkgsets/go1.23.7/global/pkg/mod/github.com/google/go-cmp@v0.7.0/cmp/report.go:45 +0xf0
  github.com/google/go-cmp/cmp.Diff()
      /Users/myuser/.gvm/pkgsets/go1.23.7/global/pkg/mod/github.com/google/go-cmp@v0.7.0/cmp/compare.go:132 +0x20c
  github.com/dhiaayachi/test-race.TestRace.func1()
      /Users/myuser/git/test-race/main_test.go:47 +0x17c
  github.com/dhiaayachi/test-race.TestRace.func4()
      /Users/myuser/git/test-race/main_test.go:78 +0x44

Previous read at 0x000102ecd2c8 by goroutine 7:
  reflect.Value.lenNonSlice()
      /Users/myuser/.gvm/gos/go1.23.7/src/reflect/value.go:1775 +0x118
  reflect.Value.Len()
      /Users/myuser/.gvm/gos/go1.23.7/src/reflect/value.go:1761 +0x580
  reflect.Value.IsZero()
      /Users/myuser/.gvm/gos/go1.23.7/src/reflect/value.go:1614 +0x588
  github.com/google/go-cmp/cmp.formatOptions.FormatValue()
      /Users/myuser/.gvm/pkgsets/go1.23.7/global/pkg/mod/github.com/google/go-cmp@v0.7.0/cmp/report_reflect.go:194 +0x2220
  github.com/google/go-cmp/cmp.formatOptions.FormatValue()
      /Users/myuser/.gvm/pkgsets/go1.23.7/global/pkg/mod/github.com/google/go-cmp@v0.7.0/cmp/report_reflect.go:298 +0x1804
  github.com/google/go-cmp/cmp.formatOptions.FormatValue()
      /Users/myuser/.gvm/pkgsets/go1.23.7/global/pkg/mod/github.com/google/go-cmp@v0.7.0/cmp/report_reflect.go:205 +0x240c
  github.com/google/go-cmp/cmp.formatOptions.FormatValue()
      /Users/myuser/.gvm/pkgsets/go1.23.7/global/pkg/mod/github.com/google/go-cmp@v0.7.0/cmp/report_reflect.go:205 +0x240c
  github.com/google/go-cmp/cmp.formatOptions.FormatValue()
      /Users/myuser/.gvm/pkgsets/go1.23.7/global/pkg/mod/github.com/google/go-cmp@v0.7.0/cmp/report_reflect.go:298 +0x1804
  github.com/google/go-cmp/cmp.formatOptions.FormatValue()
      /Users/myuser/.gvm/pkgsets/go1.23.7/global/pkg/mod/github.com/google/go-cmp@v0.7.0/cmp/report_reflect.go:308 +0x1180
  github.com/google/go-cmp/cmp.formatOptions.FormatDiff()
      /Users/myuser/.gvm/pkgsets/go1.23.7/global/pkg/mod/github.com/google/go-cmp@v0.7.0/cmp/report_compare.go:144 +0x2068
  github.com/google/go-cmp/cmp.(*defaultReporter).String()
      /Users/myuser/.gvm/pkgsets/go1.23.7/global/pkg/mod/github.com/google/go-cmp@v0.7.0/cmp/report.go:45 +0xf0
  github.com/google/go-cmp/cmp.Diff()
      /Users/myuser/.gvm/pkgsets/go1.23.7/global/pkg/mod/github.com/google/go-cmp@v0.7.0/cmp/compare.go:132 +0x20c
  github.com/dhiaayachi/test-race.TestRace.func2()
      /Users/myuser/git/test-race/main_test.go:56 +0xe8
  github.com/dhiaayachi/test-race.TestRace.func3()
      /Users/myuser/git/test-race/main_test.go:65 +0x44

Goroutine 8 (running) created at:
  github.com/dhiaayachi/test-race.TestRace()
      /Users/myuser/git/test-race/main_test.go:72 +0x17c
  testing.tRunner()
      /Users/myuser/.gvm/gos/go1.23.7/src/testing/testing.go:1690 +0x184
  testing.(*T).Run.gowrap1()
      /Users/myuser/.gvm/gos/go1.23.7/src/testing/testing.go:1743 +0x40

Goroutine 7 (running) created at:
  github.com/dhiaayachi/test-race.TestRace()
      /Users/myuser/git/test-race/main_test.go:58 +0xdc
  testing.tRunner()
      /Users/myuser/.gvm/gos/go1.23.7/src/testing/testing.go:1690 +0x184
  testing.(*T).Run.gowrap1()
      /Users/myuser/.gvm/gos/go1.23.7/src/testing/testing.go:1743 +0x40
==================

Comment From: Jorropo

Am I missing where I can find this program ?

Comment From: dhiaayachi

Sorry missed adding the link, updated!

Comment From: gabyhelp

Related Issues

(Emoji vote if this was helpful or unhelpful; more detailed feedback welcome in this discussion.)

Comment From: Jorropo

Your library is accessing time.loc.* there are two levels of accessing private fields (this matters because you can't have this data race if you use == between time.Time types since time.loc is a pointer altho I don't know if it timezone comparisons would return accurate results then).

Our usual stance on issues relating to accessing private fields of stdlib structs is fixing this would set a bad precedent because it would severely limit our ability to do any kind of modification to the internals of the stdlib.

I am gonna close this but if someone thinks this can be solved and this is something we want to do it could be reopened.

Comment From: dhiaayachi

Thank you for the explanation @Jorropo!

My understanding is that even if time.Time contain a pointer, it's considered safe to shallow copy as it's immutable and by extension it's also correct to perform an == on it, right?

So based on that, if I understand right, your suggestion is to "fix" go-cmp to treat time.Time as a special case and avoid walking through its fields and use == when comparing time.Time?

btw, I don't have any affiliation with go-cmp, I'm just a user of the library.

Comment From: randall77

I kind of feel like this is a problem. Not sure how to fix it though. The problem is we have paths that get around this sync.Once.

Here's a simplified example:

package main

import (
    "reflect"
    "time"
)

func main() {
    c := make(chan bool)
    go func() {
        println(time.Local.String())
        c <- true
    }()
    go func() {
        println(reflect.DeepEqual(time.Local, time.UTC))
        c <- true
    }()
    <-c
    <-c
}

The first goroutine goes through the sync.Once to initialize the local time zone. The latter goes through reflect so it doesn't know that there is a sync.Once guarding access to the time zone info.

I'm going to reopen. But I'm not sure how we might go about fixing this. Or even if we should. Jorropo's warning about accessing internal fields is pertinent here.

Maybe we need to move some of this sync.Once work into an init function? That would help, I think, but maybe there are reasons we don't want to do this processing on startup for ~every binary in existence.

Comment From: randall77

(As a practical matter, you can add time.Local.String(), or probably any other time function, at the start of main.main and this problem will go away.)

Comment From: ianlancetaylor

In general it's not reliable to use reflect.DeepEqual with types that you don't control, because it compares unexported struct fields. That means that the result of reflect.DeepEqual can change if the package changes the ways that it uses unexported fields.

And, of course, reflect.DeepEqual does not work for time.Time values, for the reasons given at https://pkg.go.dev/time#Time in the discussion of the == operator.

I don't think there is anything to do here.

Comment From: seankhliao

this looks like another instance of #71732

Comment From: Jorropo

It's a bit different to #71732 since this code never calls reflect.DeepEqual.