Reproducible data-race scenarios of Go. It's based on the Uber blog: Data Race Patterns in Go.
Go's deliberately lightweight concurrency model makes concurrent code easy to write. But easy does not mean simple, ownership and synchronization remain largely the developer's responsibility.
Go developers are often unaware that a variable used inside a closure is a free variable captured by reference, especially when the closure is large. They also frequently use closures as goroutines. As a result, captured variables may be accessed concurrently and without an ordering guarantee unless explicit synchronization is used.
This project demonstrates these risks, their idiomatic fixes and how Java and C++ handle similar situations.
- GNU Make and a POSIX shell
- Go 1.26.x (
make check-gorefuses any other compiler) - JDK/Javac 21 and Maven >= 3.6.3
- CMake >= 3.16 and a C++20 compiler (g++/clang++)
| Command | Purpose |
|---|---|
make list |
list the seven scenarios |
make demo SCENARIO_LANG=go PATTERN=01-closure-capture |
run one scenario demo (go/java/cpp) |
make check-go |
Go 1.26.x gate, build all with -race, run the fixed tests, run the 7 expected-race demos |
make check-java |
build Java, run the 7 Fixed classes, run the jcstress probe (no FORBIDDEN outcome) |
make check-cpp |
CMake/TSan build, run every fixed executable (no race report) |
make check-all |
check-go + check-java + check-cpp |
make jcstress |
visible JCStress litmus run, results kept under artifacts/jcstress/ |
SCENARIO_LANG selects the demo language (it is not the POSIX LANG variable); PATTERN selects the scenario.
An unsafe Go/C++ demo succeeds only when the expected WARNING: DATA RACE (Go) or WARNING: ThreadSanitizer: data race (C++) line is really observed (scripts/run-expected-race.sh).
Every scenario ships a racy unsafe program and a corrected fixed program under scenarios/<pattern>/<lang>/, plus a scenario
README.
unsafe is caught by a race detector (Go -race, C++ TSan) and fixed
runs clean under it.
Java cannot always reproduce the Go traps, due to its design:
- scenario 01 is prevented at compile time (javac rejects the reassigned capture)
- scenario 04 is structurally not applicable (Java never copies an Object monitor)
| # | Pattern | What it shows | README |
|---|---|---|---|
| 01 | closure capture | a closure reads an outer mutable variable by reference (Go 1.22+ fixed only the loop variable); Java: compile-time prevention | README |
| 02 | slice sharing | copying a slice header shares the backing array; Java: exact | README |
| 03 | map access | concurrent access to a Go map, HashMap, unordered_map; Java: exact |
README |
| 04 | copied lock | a value receiver copies the mutex; Java: not applicable; C++: does not compile | README |
| 05 | signal vs payload | the signal (channel/latch) and the payload field are not synchronized together | README |
| 06 | group synchronization | Add/countDown happens after Wait/await started |
README |
| 07 | parallel tests | t.Parallel sub-tests mutate shared package state |
README |
- Go Memory Model: https://go.dev/ref/mem
- Go race detector: https://go.dev/doc/articles/race_detector
- Java Memory Model (JLS ch. 17): https://docs.oracle.com/javase/specs/jls/se21/html/jls-17.html
- C++ memory model: https://en.cppreference.com/w/cpp/atomic/memory_order
- ThreadSanitizer: https://clang.llvm.org/docs/ThreadSanitizer.html