Go tip of the week: Concurrency and pointers

“Don't communicate by sharing memory, share memory by communicating.”

This Go proverb describes a powerful concept: Communicating Sequential Processes (CSP).

Don't try sharing data between goroutines through a shared memory area that is guarded by an error-prone pile of intertwined mutexes. You quickly get into concurrent debugging hell.

Rather, have goroutines use their own local memory and exchange data by sending values around.

This works well… until you decide to create a channel of maps. Or a channel of slices. Suddenly, you get data races.

Why?

Maps and slices are data types that contain pointers under the hood. Those pointers refer to places in memory that hold the actual data.

And here is the gotcha:

Go strictly passes data by value. (Whether the data is passed to a function or through a channel.)

A pointer is just a value. In fact, a pointer is nothing but an unsigned integer that happens to represent the address of a memory cell.

So when Go sends a map or a slice through a channel, the internal pointer gets copied along. No deep cloning happens. (Because that's not part of the deal. Go has no reason to treat a pointer value as something special. Want a deep clone of a complex data structure? It's upon you to make that happen.)

Now two pointers exist in different goroutines that point to the same memory location. One in the sending goroutine, and another in the receiving goroutine. Congrats, now you have unprotected shared memory. The code is open for data races. The last update silently wins.

TL;DR:

Never send anything through a channel that contains a pointer. No maps, no slices, no structs with pointer fields.

Send primitive data types, strings (they are immutable), structs with plain value fields, or arrays.

P.S. Pro Tip: Whenever your code contains concurrency, Go's race detector is your friend.