This is one of my article which made me explore the whole Google in pursuit of which methods is best to make synchronization work well in GoLang. All this started when I started building my own GoLang package, Go-Log. It’s a logging package which provides utility on top of Go’s normal Log package with features like — Tagging the logs into debug and error variant, adding/removing timestamp to logs and also getting the calling function details in logs. However, there was a great need to make this logging thread safe or you can there was a need of synchronization since when millions of request come to the server logs needs to be synchronized with less latency.
While surfing the articles, StackOverflow Q/A, videos in an idiomatic Go style, I came across two methods —
1. Coordinating with Channels which are already thread-safe
2. Coordinating using Mutex over shared memory
The most common mistake a Go developer does?
Before starting with a discussion on which method to pick, you all need to know a common mistake as a Go Developer you all are doing. I know you all are fantasized by the concurrency pattern which leads you to use the superpower of Go — Channels and Goroutines excessively which becomes an anti-pattern in the end. I am not saying Channels are bad or they must not be used for synchronization, but excessive use (using it where there is no requirement) of this is definitely not a path you should follow.
Dave Cheney, an open source contributor and project member for the Go programming language, once in an interview said “I tried to use channels for everything if you want to talk about worst code”
Go’s official documentation states that “A common Go newbie mistake is to over-use channels and goroutines just because it’s possible, and/or because it’s fun.”
Now, assuming you certainly not have any pre-notions about goroutines and channels or mutex at all, let's see the difference between the two approaches mentioned earlier on how to do synchronisation.
Communication using channels to achieve synchronisation
Channels are the pipes that connect concurrent goroutines. You can send values into channels from one goroutine and receive those values into another goroutine.
Channels are best suited in cases like passing ownership of data, distributing units of work and communicating async results.
Let’s make use of these channels to achieve synchronisation for my Go Lang package(Aim is to make logs work synchronously and thread safe). Below is a code snippet from my package which I wrote earlier.
I have used basic channel communication to synchronise multiple goroutines which might occur when sever logs million times. Best part about channels is that they have in built thread safety and prevent race conditions.
If you carefully look at my case and ask was there a definite need of channels for this? Answer is NO. Channels are high level concept in Go which somewhere inside use Mutex only. Go channels are appealing because they provide built-in thread safety and encourage single-threaded access to shared critical resources. Channels, however, incur a performance penalty compared to mutexes. Using mutexes is helpful when you just need locks over few shared resources. Don’t be afraid to use a
sync.Mutex if that fits your problem best.
How I used Mutex to make logging synchronous?
The Mutex (mutual exclusion lock) is an invaluable resource when synchronising state across multiple goroutines, but I find that it’s usage is somewhat mystifying to new Go developers. It’s pretty easy to use!
Here is a code snippet from my package, Go-Log
I have used mutex.Lock() and mutex.Unlock() to create a synchronous lock over a shared resource. And to manage multiple logs I create a goroutine every time and added them to sync.WaitGroup. These are an important synchronisation primitive. These allow co-operating goroutines to collectively wait for a threshold event before proceeding independently again.
In our case, this approach is much better and faster! We reduced the un necessary overhead. However, if you ever find your sync.Mutex locking rules are getting too complex, ask yourself whether using channel(s) might be simpler.
Check out the source code of my package to learn how I actually implemented the mutexes to achieve synchronisation —