Go version
go version go1.25rc1 linux/amd64
Output of go env
in your module/workspace:
AR='ar'
CC='gcc'
CGO_CFLAGS='-O2 -g'
CGO_CPPFLAGS=''
CGO_CXXFLAGS='-O2 -g'
CGO_ENABLED='1'
CGO_FFLAGS='-O2 -g'
CGO_LDFLAGS='-O2 -g'
CXX='g++'
GCCGO='gccgo'
GO111MODULE=''
GOAMD64='v1'
GOARCH='amd64'
GOAUTH='netrc'
GOBIN=''
GOCACHE='/home/terin/.cache/go-build'
GOCACHEPROG=''
GODEBUG=''
GOENV='/home/terin/.config/go/env'
GOEXE=''
GOEXPERIMENT=''
GOFIPS140='off'
GOFLAGS=''
GOGCCFLAGS='-fPIC -m64 -pthread -Wl,--no-gc-sections -fmessage-length=0 -ffile-prefix-map=/tmp/go-build2428105469=/tmp/go-build -gno-record-gcc-switches'
GOHOSTARCH='amd64'
GOHOSTOS='linux'
GOINSECURE=''
GOMOD='/home/terin/Development/github.com/cloudflare/certinel/go.mod'
GOMODCACHE='/home/terin/go/pkg/mod'
GONOPROXY=''
GONOSUMDB=''
GOOS='linux'
GOPATH='/home/terin/go'
GOPRIVATE=''
GOPROXY='https://proxy.golang.org,direct'
GOROOT='/home/terin/sdk/go1.25rc1'
GOSUMDB='sum.golang.org'
GOTELEMETRY='local'
GOTELEMETRYDIR='/home/terin/.config/go/telemetry'
GOTMPDIR=''
GOTOOLCHAIN='auto'
GOTOOLDIR='/home/terin/sdk/go1.25rc1/pkg/tool/linux_amd64'
GOVCS=''
GOVERSION='go1.25rc1'
GOWORK=''
PKG_CONFIG='pkg-config'
What did you do?
I've written a test using testing/synctest
that I expected to be free of data races. https://go.dev/play/p/FKlttCmxPIz?v=gotip
What did you see happen?
WARNING: DATA RACE
Write at 0x00c000124430 by goroutine 10:
github.com/cloudflare/certinel/ticker.TestTickerSync.func1.1()
/home/terin/Development/github.com/cloudflare/certinel/ticker/ticker_go125_test.go:20 +0x72
Previous read at 0x00c000124430 by goroutine 9:
github.com/cloudflare/certinel/ticker.TestTickerSync.func1()
/home/terin/Development/github.com/cloudflare/certinel/ticker/ticker_go125_test.go:30 +0x191
testing.tRunner()
/home/terin/sdk/go1.25rc1/src/testing/testing.go:1931 +0x21c
testing.testingSynctestTest.gowrap1()
/home/terin/sdk/go1.25rc1/src/testing/testing.go:2043 +0x44
What did you expect to see?
No data race as the read of variable errWatcher
happened after synctest.Wait
and the goroutine that writes to it was (to my current understanding) durably blocked.
Comment From: seankhliao
I believe the race detector report is correct. There is nothing that establishes a ordering between the assignment to the error and the subsequent read, see synchronization for what are considered valid events that do so. synctest.Wait is not a synchronization primitive.
Comment From: terinjokes
At GopherCon EU @neild gave a presentation on synctest. Part of the presentation was synctest.Wait working in conjunction with the race detector to make it easier to write tests like the one I wrote in Play.
Certainly possible I misunderstood the slide, but if I recall correctly it was writing to a value in a goroutine and then reading it after a synctest.Wait. Thst seems pretty similar to what I've done.
Comment From: neild
Hm, there's something strange here. If I reduce the test to this, I see a data race on the indicated lines:
func Test(t *testing.T) {
synctest.Test(t, func(t *testing.T) {
var v bool
go func() {
time.Sleep(2 * time.Minute)
v = true // data race: write
}()
time.Sleep(30 * time.Second)
synctest.Wait()
_ = v // data race: previous read
synctest.Wait()
time.Sleep(90 * time.Second)
})
}
That doesn't seem correct, since there are synctest.Wait
calls before and after the supposedly racy read.
I'm not sure what's going on yet, but unless I'm missing something there's a bug in the interaction between Wait
and the race detector.
Comment From: seankhliao
my understanding was that there was no synchronization between the read/write so the race is expected. to synchronize, you'd have to put the Wait after the 2min sleep in the goroutine
Comment From: neild
Ah, no, I was missing something (and @seankhliao is right).
synctest.Wait
waits for the bubble to become durably blocked, and then creates a happens-before relationship with all activity up to that point in time and the Wait
call.
For the reduced case I gave, the Wait
calls establish that the _ = v
read happens before the time.Sleep
call, but nothing establishes the ordering of the v = true
write.
In the original example (https://go.dev/play/p/FKlttCmxPIz?v=gotip), a similar problem exists: Nothing establishes the ordering of the write to errWatcher
.
Comment From: neild
I do still feel like something isn't behaving quite as it should here, though. The Wait
calls in the reduced test are clearly describing a non-racy sequence of events where the read at 30s happens before the write at 120s.
Comment From: terinjokes
There may be a race between the write and the second read in my example.
Putting that aside, why is a race reported between the read and the writes in the reduced example? My mental model is that the first read should have been sequenced and established with the existing synctest.Wait
s before the deadline was slept to?
Comment From: seankhliao
The read happens after 30s, but may be delayed arbitrarily far into the future, e.g. due to resource starvation, such that it happens in parallel with the write. This usually doesn't happen, but it is a valid sequence of events.
Comment From: neild
Filed #74352 so we don't forget about this. The current behavior is working-as-intended, but I wonder if we can tweak the race detector integration here to avoid reporting races in more cases that use synctest.Wait
, without compromising the detection of real races in unsynchronized goroutines.