Go version

go version go1.24.6 darwin/arm64

Output of go env in your module/workspace:

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/<redacted>/Library/Caches/go-build'
GOCACHEPROG=''
GODEBUG=''
GOENV='/Users/<redacted>/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/s_/3pdj5n_55mn_tt4ml59h9jlh0000gp/T/go-build161577767=/tmp/go-build -gno-record-gcc-switches -fno-common'
GOHOSTARCH='arm64'
GOHOSTOS='darwin'
GOINSECURE=''
GOMOD='/Users/<redacted>/Developer/P20457-Foundation-Inventory/go.mod'
GOMODCACHE='/Users/<redacted>/go/pkg/mod'
GONOPROXY='dev.azure.com/INGCDaaS'
GONOSUMDB='dev.azure.com/INGCDaaS'
GOOS='darwin'
GOPATH='/Users/<redacted>/go'
GOPRIVATE=‘<redacted>
GOPROXY='https://proxy.golang.org,direct'
GOROOT='/usr/local/go'
GOSUMDB='sum.golang.org'
GOTELEMETRY='local'
GOTELEMETRYDIR='/Users/<redacted>/Library/Application Support/go/telemetry'
GOTMPDIR=''
GOTOOLCHAIN='auto'
GOTOOLDIR='/usr/local/go/pkg/tool/darwin_arm64'
GOVCS=''
GOVERSION='go1.24.6'
GOWORK=''
PKG_CONFIG='pkg-config'

What did you do?

When I run tests, I sometimes run into a datarace. The command I run is: go test -coverpkg=./... -coverprofile=covertest.out -covermode=atomic -count=1 ./... -race -parallel 10 -shuffle=on

What did you see happen?

The full output of in the datarace is here: datarace.txt.

The summary is:

==================
WARNING: DATA RACE
Read at 0x00c0001031c3 by goroutine 21:
  testing.(*common).logDepth()
      /usr/local/go/src/testing/testing.go:1053 +0x8c
  testing.(*common).log()
      /usr/local/go/src/testing/testing.go:1046 +0x80
  testing.(*common).Logf()
      /usr/local/go/src/testing/testing.go:1097 +0x58
  <private repo>/pkg/test_helpers/v2.(*TestSuite).TearDown()
     <private repo>/pkg/test_helpers/v2/suite.go:349 +0xf8
  <private repo>/integrationtest/failuretest.TestIntermittentVcenterFailure.func1()
      <private repo>/integrationtest/failuretest/intermittent_vcenter_failure_test.go:44 +0x44
  testing.(*common).Cleanup.func1()
      /usr/local/go/src/testing/testing.go:1211 +0x140
  testing.(*common).runCleanup()
      /usr/local/go/src/testing/testing.go:1445 +0x1e0
  testing.tRunner.func2()
      /usr/local/go/src/testing/testing.go:1786 +0x48
  runtime.deferreturn()
      /usr/local/go/src/runtime/panic.go:610 +0x5c
  net/http/internal.(*chunkedWriter).Write()
      /usr/local/go/src/net/http/internal/chunked.go:245 +0x90
  io.copyBuffer()
      /usr/local/go/src/io/io.go:431 +0x258
  net/http/internal.(*chunkedWriter).Write()
      /usr/local/go/src/net/http/internal/chunked.go:248 +0xc0
  net/http/internal.(*chunkedWriter).Write()
      /usr/local/go/src/net/http/internal/chunked.go:245 +0x90
  (above block starting with io.copyBuffer repeats a few hundred times)

Previous write at 0x00c0001031c3 by goroutine 22:
  testing.tRunner.func1()
      /usr/local/go/src/testing/testing.go:1779 +0x5dc
  runtime.deferreturn()
      /usr/local/go/src/runtime/panic.go:610 +0x5c
  net/http/internal.(*chunkedWriter).Write()
      /usr/local/go/src/net/http/internal/chunked.go:245 +0x90
  io.copyBuffer()
      /usr/local/go/src/io/io.go:431 +0x258
  net/http/internal.(*chunkedWriter).Write()
      /usr/local/go/src/net/http/internal/chunked.go:248 +0xc0
  net/http/internal.(*chunkedWriter).Write()
      /usr/local/go/src/net/http/internal/chunked.go:245 +0x90
  (above block starting with io.copyBuffer repeats a few hundred times)

Goroutine 21 (running) created at:
  testing.(*T).Run()
      /usr/local/go/src/testing/testing.go:1851 +0x684
  testing.runTests.func1()
      /usr/local/go/src/testing/testing.go:2279 +0x7c
  testing.tRunner()
      /usr/local/go/src/testing/testing.go:1792 +0x180
  testing.runTests()
      /usr/local/go/src/testing/testing.go:2277 +0x77c
  testing.(*M).Run()
      /usr/local/go/src/testing/testing.go:2142 +0xb68
  <private repo>/integrationtest/failuretest.TestMain()
      <private repo>/integrationtest/failuretest/setup_test.go:16 +0x150
  main.main()
      _testmain.go:61 +0x114

Goroutine 22 (finished) created at:
  testing.(*T).Run()
      /usr/local/go/src/testing/testing.go:1851 +0x684
  testing.runTests.func1()
      /usr/local/go/src/testing/testing.go:2279 +0x7c
  testing.tRunner()
      /usr/local/go/src/testing/testing.go:1792 +0x180
  testing.runTests()
      /usr/local/go/src/testing/testing.go:2277 +0x77c
  testing.(*M).Run()
      /usr/local/go/src/testing/testing.go:2142 +0xb68
 <private repo>/integrationtest/failuretest.TestMain()
      <private repo>/integrationtest/failuretest/setup_test.go:16 +0x150
  main.main()
      _testmain.go:61 +0x114
==================

All repsitories are private so I can only share part of the source code. Below are excerpts of the source code referenced in the datarace stacktrace

// file /pkg/test_helpers/v2/suite.go

// TearDown removes the resources and network created for the test suite.
func (t *TestSuite) TearDown(ctx context.Context) error {
        var finalErr error

        var numResources int

        t.mu.Lock()
        numResources = len(t.Resources)

        // the line below was marked as a datarace which I do not understand
        t.logger.Logf("tearing down %d containers", numResources) // <- suite.go:349. !!! This triggers the read action. The logger property is an interface that is fulfilled by testing.T and consists of methods Log and Logf.
        t.mu.Unlock()    

        for _, resource := range t.Resources {
                if err := resource.Purge(ctx); err != nil {
                        finalErr = errors.Join(err)
                }
        }

        t.logger.Log("remove network")

        if err := t.Pool.RemoveNetwork(t.Network); err != nil {
                finalErr = errors.Join(err)
        }

        t.teardownComplete <- struct{}{}

The above TearDown is called from the t.Cleanup method in the functino below:

// file /integrationtest/failuretest/intermittent_vcenter_failure_test.go
func TestIntermittentVcenterFailure(t *testing.T) {

        t.Parallel()

        ctx := t.Context()

        testSuite, err := testHelpers.NewTestSuite(testHelpers.WithLogger(t))

        t.Cleanup(func() {
                // stop containers
                _ = testSuite.TearDown(ctx) // <- intermittent_vcenter_failure_test.go:44
        })

Goroutines 21 and 22 are started by /integrationtest/failuretest/setup_test.go:16. The source code for this is:

// file setup_test.go
package failuretest

import (
        "flag"
        "testing"
)

var noTearDown, updateGoldenFiles bool

func TestMain(m *testing.M) {
        flag.BoolVar(&noTearDown, "noteardown", false, "keep containers after finishing test")
        flag.BoolVar(&updateGoldenFiles, "update", false, "update golden files")
        flag.Parse()
        m.Run() // <- setup_test.go:16
}

What did you expect to see?

I was not expecting a datarace.

I have spend quite some time finding a solution and replicating the issue in an application that I can share but I was not successful.

The two conflicting go routines are started by testing.M.Run which is not under my control. I cannot manipulate the testing.T object because that is a given. My conclusion up to this point is that this datarace is outside of my control. Maybe it is a bug or maybe one should not write with t.Log in a Cleanup function. However I tried to trigger a datarace in a heavily stripped down version of my test but I was not able to replicate this.

Comment From: seankhliao

Unlike many projects, the Go project does not use GitHub Issues for general discussion or asking questions. GitHub Issues are used for tracking bugs and proposals only.

For questions please refer to https://github.com/golang/go/wiki/Questions

Comment From: ArnoSen

Thanks for the feedback. I know that this is not for general discussion or asking questions. To me this is not a general discussion or asking a generic question. This is about a potential bug in go core library. As mentioned the datarace refers to objects in the std library that are out of control of the user.