Golang: avoiding race conditions

Issue

What are some good practices to prevent race conditions in Go?

The only one I can think of is not sharing data between goroutines – the parent goroutine sends a deep copy of an object rather than the object itself, so the child goroutine can’t mutate something that the parent can. This would use up more heap memory, but the other alternative is to learn Haskell 😛

Edit: also, is there any scenario in which the method I described above can still run into race conditions?

Solution

Race conditions can certainly still exist even with unshared data structures. Consider the following:

B asks A for the currentCount
C asks A for the currentCount
B sends A (newDataB, currentCount + 1)
A stores newDataB at location currentCount+1
C sends A (newDataC, currentCount + 1)
A stores newDataC at currentCount + 1 (overwriting newDataB; race condition)

This race condition requires private mutable state in A, but no mutable shared data structures and doesn’t even require mutable state in B or C. There is nothing B or C can do to prevent this race condition without understanding the contract that A offers.

Even Haskell can suffer these kinds of race conditions as soon as state enters the equation, and state is very hard to completely eliminate from a real system. Eventually you want your program to interact with reality, and reality is stateful. Wikipedia gives a helpful race condition example in Haskell using STM.

I agree that good immutable data structures could make things easier (Go doesn’t really have them). Mutable copies trade one problem for another. You can’t accidentally change someone else’s data. On the other hand, you may think that you’re changing the real one, when you’re actually just changing a copy, leading to a different kind of bug. You have to understand the contract either way.

But ultimately, Go tends to follow the history of C on concurrency: you make up some ownership rules for your code (like @tux21b offers) and make sure you always follow them, and if you do it perfectly it’ll all work great, and if you ever make a mistake, then obviously it’s your fault, not the language.

(Don’t get me wrong; I like Go, quite a lot really. And it offers some nice tools to make concurrency easy. It just doesn’t offer many language tools to help make concurrency correct. That’s up to you. That said, tux21b’s answer offers lots of good advice, and the race detector is definitely a powerful tool for reducing race conditions. It’s just not part of the language, and it’s about testing, not correctness; they’re not the same thing.)

EDIT: To the question about why immutable data structures make things easier, this is the extension of your initial point: creating a contract where multiple parties don’t change the same data structure. If the data structure is immutable, then that comes for free…

Many languages have a rich set of immutable collections and classes. C++ lets you const just about anything. Objective-C has immutable collections with mutable subclasses (which creates a different set of patterns than const). Scala has separate mutable and immutable versions of many collection types, and it is common practice to use the immutable versions exclusively. Declaring immutability in a method signature is an important indication of the contract.

When you pass a []byte to a goroutine, there is no way to know from the code whether the goroutine intends to modify the slice, nor when you may modify the slice yourself. There a patterns emerging, but they’re like C++ object ownership before move semantics; lots of fine approaches, but no way to know which one is in use. It’s a critical thing that every program needs to do correctly, yet the language gives you no good tools, and there is no universal pattern used by developers.

Answered By – Rob Napier

Answer Checked By – Terry (GoLangFix Volunteer)

Leave a Reply

Your email address will not be published.