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
- 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.
- Run the race detector.
go build -raceuses a ThreadSanitizer based detector that reports a race, and an implementation may halt on one. - Make data immutable when you can. Build a value, publish it once, and never mutate it after other goroutines can see it.
- Keep maps behind owners. One goroutine owns a map and others ask it through a channel, or a mutex guards every access.
- 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.