Safeguard
Security Guides•

Go data races can corrupt memory: what the memory model says

Go is called memory safe, yet its memory model says races on slices, maps, strings and interfaces can corrupt memory. Here is what to do about that.

Safeguard Security Team
4 min read

Go is on most memory safe language lists, but its memory model says that data races on multiword values such as interfaces, maps, slices and strings can lead to arbitrary memory corruption. The fix is to serialize shared access and to run tests with the race detector.

What the model says

For words that fit a single machine word, each read must see a value written by a preceding or concurrent write, and out of thin air values are disallowed. Larger values are different. The model says implementations may treat larger operations as several word sized operations in an unspecified order, so races on multiword structures can show inconsistent values that do not match any single write.

Then comes the security relevant sentence. When values depend on the consistency of internal pointer and length pairs, or pointer and type pairs, as in interface values, maps, slices and strings in most implementations, such races can lead to arbitrary memory corruption.

Why this matters

A slice is a pointer, a length and a capacity. If one goroutine updates it while another reads it, the reader can see a new pointer with an old length. That is the shape of an out-of-bounds access, in a language that has no unsafe keyword in sight.

What to do

  1. Serialize access. The model's advice is that programs modifying data accessed by multiple goroutines must serialize that access, using channels or the sync package.
  2. Run the race detector. go build -race uses a ThreadSanitizer based detector that reports a race, and an implementation may halt on one.
  3. Make data immutable when you can. Build a value, publish it once, and never mutate it after other goroutines can see it.
  4. Keep maps behind owners. One goroutine owns a map and others ask it through a channel, or a mutex guards every access.
  5. Run race tests in CI, not only locally, and include the packages with the most concurrency.

What the detector cannot do

A race detector reports races that occur during a run. It cannot find one in a code path your tests never exercise, so concurrency heavy code needs tests that push many goroutines through the same paths.

Spotting risky patterns in review

Look for package level variables that hold slices or maps and are written from a request handler. Look for a struct shared between goroutines with no lock, where one goroutine appends and another ranges. Look for a lazily initialized global that two goroutines can initialize at once.

Also look at code that copies an interface value or a string out of shared state without a guard. The memory model's concern is about consistency of the pair of words inside those values, so even a read can be unsafe if a write races with it.

What to measure

Count race detector failures in CI per month and track how many were fixed within a sprint. Count the number of shared mutable globals in your code base, and treat reducing it as a design goal. Fewer shared mutable values means fewer places where the guarantee depends on discipline.

Relation to other languages

The same shape exists elsewhere: a safe language whose guarantee depends on a rule the programmer must follow. In Rust the rule is about unsafe blocks. In Go it includes race freedom for multiword values. Knowing the rule is what makes the guarantee usable.

Quick check

Is Go still a good choice for memory safety?

Yes for most purposes. The guarantee is conditional on race free code, so build the race checks into your workflow.

Sources

Last reviewed: 19 October 2026.

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.

Self-healing security runs on Safeguard.

Your first fix PR is minutes away.

No sales call required, even your agent can complete the purchase over MCP.