A million ways to die from a data race in Go
Concerns over how easy it is to introduce data races in Go are prompting scrutiny of the language’s concurrency model, tooling, and design choices. Commenters argue that while Go advertises “share memory by communicating” and offers powerful goroutines and channels, its mutable shared state, lack of immutability markers, and subtle pitfalls around closures and mutex lifetimes make safe concurrent code surprisingly hard—especially in large teams. Comparisons with Rust, Erlang/Elixir, Java, and C# highlight a broader trade-off between permissive, performant systems programming and stronger compile-time guarantees that prevent entire classes of concurrency bugs.
Value and validity of the article
- Several commenters find the examples realistic, matching issues they’ve debugged in real Go code and other writeups (e.g. Uber’s race patterns).
- Others argue some examples are “beginner-level” or even wrong (e.g. per‑request mutex protecting shared data, odd “fixes”), casting doubt on the claimed experience.
- There’s disagreement whether this is “crapping on Go” or necessary documentation of real pitfalls, especially for newcomers.
Go’s concurrency model and data races
- Go’s slogan “don’t communicate by sharing memory…” is seen as aspirational: goroutines all share heap memory, so it’s easy to accidentally share mutable state.
- Many note that Go does not enforce message passing; you must voluntarily avoid shared mutability using channels and good patterns.
- Others counter that threads/goroutines by definition share memory; coordination via mutexes, atomics, queues is normal and expected in a low‑level language.
Language design, tooling, and footguns
- The
:=shadowing/closure example is broadly acknowledged as a real footgun; Go offers no language-level protection and relies on human care and IDE highlighting. - Critics argue this is precisely what modern language design should prevent from compiling; proponents say “you must understand the memory model and docs”.
- Some praise Go tooling (race detector, IDE support); others say it’s nowhere near Java’s or even below average, with weaker debugging and heavier language server.
Comparisons to other ecosystems
- Rust: similar code would be compile‑time errors; the borrow checker primarily prevents data races, not just memory leaks. Unsafe code can still race, but is localized.
- JVM / .NET: data races can cause logical bugs but not corrupt the runtime; this is contrasted with Go’s potential for memory issues via races on “fat pointers” like slices.
- Java/Kotlin: immutable HTTP clients and structured concurrency reduce entire classes of bugs.
- Haskell, Erlang/Elixir, Rust are cited as languages that largely prevent these races by design.
APIs, mutability, and http.Client
- The
http.Clientexample splits opinions: some say “concurrent use vs modification” is clear and consistently documented; others find such linguistic distinctions too subtle for concurrency safety. - Several wish Go had explicit immutability (immutable structs/fields, builder patterns) or clearer “Sync*” types to make safe sharing obvious.
Erlang/Elixir and alternative models
- Elixir/BEAM are described as eliminating data races via immutability and isolated processes; you still get logical races, deadlocks, leaks, and resource exhaustion, but not memory‑model violations.
- Compared to Go, Elixir is viewed as far better suited for highly concurrent network services, at the cost of being less general‑purpose.
Power vs safety vs productivity
- Some argue any powerful language must allow you to “shoot yourself in the foot”; otherwise it’s dismissed as a toy.
- Others respond that Go’s combination of non‑thread‑safe defaults with a trivial
gokeyword is an unsafe default, especially for large teams. - There’s a recurring sentiment that Go makes you feel productive (fast compiles, simple syntax), while hiding substantial concurrency hazards.