Actors in Sync: Sharing state and communication in Go
“Do not communicate by sharing memory; instead, share memory by communicating.” —Effective Go
I recently wrote a simple proxy for a service which required an authentication token to be sent with each request. The token would need to be refreshed periodically, and would need to be shared with goroutines handling the incoming requests.
I went through three iterations for a token storage mechanism which I think nicely demonstrate how I misused channels.
In the first iteration, I created two global channels. One to receive the token from storage, and one to ask for the token to be refreshed. A function like this one is then called in a goroutine:
Get a new token on refresh.
Set up a refresh in the future.
Send the token to anyone who wants it.
This protects the token variable from concurrent reads and writes.
This first pass has a few problems. The biggest one being that, during a refresh, the old but still valid token is not sent to requests that are asking for it.
Let’s fix that. Here’s the second iteration:
Keep serving the old token.
Stop the interim goroutine.
The tokener is getting a bit… complicated.
Also, there’s a potential bug. From the Go language spec:
If one or more of the communications can proceed, a single one that can proceed is chosen via a uniform pseudo-random selection.
It is possible, but very very unlikely, that the case of sending the token would “win” for too long. A bizzaro Martingale strategy, working against us, might send an expired token.
For the third iteration, I realized that using channels for communication is a good use case, but using them to share a single variable might not be.
Separating out the token store into it’s own package, and using sync.RWMutex:
I like this solution more, even if it is more verbose.
Newcomers to Go might assume that every time you share memory, you must use a channel. That’s certainly the impression I had. Don’t make that assumption. Use whatever tool is best for the job.
For communication between goroutines: usually, channels.
For sharing access to simple variables: usually, mutexes.