Go’s sync Package: A Set of Concurrency Synchronization Techniques
Daniel Hayes
Full-Stack Engineer · Leapcell

Detailed Explanation of the sync
Standard Library Package in Go Language
In the concurrent programming of the Go language, the sync
standard library package provides a series of types for implementing concurrent synchronization. These types can meet different memory ordering requirements. Compared with channels, using them in specific scenarios is not only more efficient but also makes the code implementation more concise and clear. The following will introduce in detail several commonly used types in the sync
package and their usage methods.
1. sync.WaitGroup
Type (Wait Group)
The sync.WaitGroup
is used to achieve synchronization between goroutines, allowing one or more goroutines to wait for several other goroutines to complete their tasks. Each sync.WaitGroup
value internally maintains a count, and the initial default value of this count is zero.
1.1 Method Introduction
The sync.WaitGroup
type contains three core methods:
Add(delta int)
: Used to change the count maintained by theWaitGroup
. When a positive integerdelta
is passed in, the count increases by the corresponding value; when a negative number is passed in, the count decreases by the corresponding value.Done()
: It is an equivalent shortcut forAdd(-1)
, and is usually used to decrement the count by 1 when a goroutine task is completed.Wait()
: When a goroutine calls this method, if the count is zero, this operation is a no-op (no operation); if the count is a positive integer, the current goroutine will enter a blocked state and will not re-enter the running state until the count becomes zero, that is, theWait()
method returns.
It should be noted that wg.Add(delta)
, wg.Done()
and wg.Wait()
are abbreviations of (&wg).Add(delta)
, (&wg).Done()
and (&wg).Wait()
respectively. If the call to Add(delta)
or Done()
causes the count to become negative, the program will panic.
1.2 Usage Example
package main import ( "fmt" "math/rand" "sync" "time" ) func main() { rand.Seed(time.Now().UnixNano()) // Required before Go 1.20 const N = 5 var values [N]int32 var wg sync.WaitGroup wg.Add(N) for i := 0; i < N; i++ { i := i go func() { values[i] = 50 + rand.Int31n(50) fmt.Println("Done:", i) wg.Done() // <=> wg.Add(-1) }() } wg.Wait() // All elements are guaranteed to be initialized. fmt.Println("values:", values) }
In the above example, the main goroutine sets the count of the wait group to 5 through wg.Add(N)
, and then starts 5 goroutines. Each goroutine calls wg.Done()
to decrement the count by 1 after completing the task. The main goroutine calls wg.Wait()
to block until all 5 goroutines have completed their tasks and the count becomes 0, and then continues to execute the subsequent code to print out the values of each element.
In addition, the call to the Add
method can also be split into multiple times, as shown below:
... var wg sync.WaitGroup for i := 0; i < N; i++ { wg.Add(1) // Will be executed 5 times i := i go func() { values[i] = 50 + rand.Int31n(50) wg.Done() }() } ...
The Wait
method of a *sync.WaitGroup
value can be called in multiple goroutines. When the count maintained by the corresponding sync.WaitGroup
value drops to 0, these goroutines will all receive the notification and end the blocked state.
func main() { rand.Seed(time.Now().UnixNano()) // Required before Go 1.20 const N = 5 var values [N]int32 var wgA, wgB sync.WaitGroup wgA.Add(N) wgB.Add(1) for i := 0; i < N; i++ { i := i go func() { wgB.Wait() // Wait for the broadcast notification log.Printf("values[%v]=%v \n", i, values[i]) wgA.Done() }() } // The following loop is guaranteed to execute before any of the above // wg.Wait calls end. for i := 0; i < N; i++ { values[i] = 50 + rand.Int31n(50) } wgB.Done() // Send a broadcast notification wgA.Wait() }
The WaitGroup
can be reused after the Wait
method returns. However, it should be noted that when the base number maintained by the WaitGroup
value is zero, the call to the Add
method with a positive integer argument cannot be run concurrently with the call to the Wait
method, otherwise a data race problem may occur.
2. sync.Once
Type
The sync.Once
type is used to ensure that a piece of code is only executed once in a concurrent program. Each *sync.Once
value has a Do(f func())
method, which accepts a parameter of type func()
.
2.1 Method Characteristics
For an addressable sync.Once
value o
, the call to the o.Do()
(i.e., the abbreviation of (&o).Do()
) method can be executed multiple times concurrently in multiple goroutines, and the arguments of these method calls should (but are not mandatory) be the same function value. Among these calls, only one of the argument functions (values) will be called, and the called argument function is guaranteed to exit before any o.Do()
method call returns, that is, the code inside the called argument function will be executed before any o.Do()
method returns the call.
2.2 Usage Example
package main import ( "log" "sync" ) func main() { log.SetFlags(0) x := 0 doSomething := func() { x++ log.Println("Hello") } var wg sync.WaitGroup var once sync.Once for i := 0; i < 5; i++ { wg.Add(1) go func() { defer wg.Done() once.Do(doSomething) log.Println("world!") }() } wg.Wait() log.Println("x =", x) // x = 1 }
In the above example, although 5 goroutines all call once.Do(doSomething)
, the doSomething
function will only be executed once. Therefore, "Hello" will only be output once, while "world!" will be output 5 times, and "Hello" will definitely be output before all 5 "world!" outputs.
3. sync.Mutex
(Mutex Lock) and sync.RWMutex
(Read-Write Lock) Types
Both the *sync.Mutex
and *sync.RWMutex
types implement the sync.Locker
interface type. Therefore, these two types both contain Lock()
and Unlock()
methods, which are used to protect data and prevent it from being read and modified simultaneously by multiple users.
3.1 sync.Mutex
(Mutex Lock)
- Basic Characteristics: The zero value of a
Mutex
is an unlocked mutex. For an addressableMutex
valuem
, it can only be successfully locked by calling them.Lock()
method when it is in the unlocked state. Once them
value is locked, a new lock attempt will cause the current goroutine to enter a blocked state until it is unlocked by calling them.Unlock()
method.m.Lock()
andm.Unlock()
are abbreviations of(&m).Lock()
and(&m).Unlock()
respectively. - Usage Example
package main import ( "fmt" "runtime" "sync" ) type Counter struct { m sync.Mutex n uint64 } func (c *Counter) Value() uint64 { c.m.Lock() defer c.m.Unlock() return c.n } func (c *Counter) Increase(delta uint64) { c.m.Lock() c.n += delta c.m.Unlock() } func main() { var c Counter for i := 0; i < 100; i++ { go func() { for k := 0; k < 100; k++ { c.Increase(1) } }() } // This loop is for demonstration purposes only. for c.Value() < 10000 { runtime.Gosched() } fmt.Println(c.Value()) // 10000 }
In the above example, the Counter
struct uses the Mutex
field m
to ensure that the field n
will not be accessed and modified simultaneously by multiple goroutines, ensuring the consistency and correctness of the data.
3.2 sync.RWMutex
(Read-Write Mutex Lock)
- Basic Characteristics: The
sync.RWMutex
internally contains two locks: a write lock and a read lock. In addition to theLock()
andUnlock()
methods, the*sync.RWMutex
type also hasRLock()
andRUnlock()
methods, which are used to support multiple readers to read data concurrently, but prevent the data from being used simultaneously by a writer and other data accessors (including readers and writers). The read lock ofrwm
maintains a count. When therwm.RLock()
call is successful, the count increases by 1; when therwm.RUnlock()
call is successful, the count decreases by 1; a count of zero indicates that the read lock is in the unlocked state, and a non-zero count indicates that the read lock is in the locked state.rwm.Lock()
,rwm.Unlock()
,rwm.RLock()
andrwm.RUnlock()
are abbreviations of(&rwm).Lock()
,(&rwm).Unlock()
,(&rwm).RLock()
and(&rwm).RUnlock()
respectively. - Locking Rules
- The write lock of
rwm
can only be successfully locked when both the write lock and the read lock are in the unlocked state, that is, the write lock can only be successfully locked by at most one data writer at any time, and the write lock and the read lock cannot be locked simultaneously. - When the write lock of
rwm
is in the locked state, any new write lock or read lock operation will cause the current goroutine to enter a blocked state until the write lock is unlocked. - When the read lock of
rwm
is in the locked state, a new write lock operation will cause the current goroutine to enter a blocked state; and a new read lock operation will be successful under certain conditions (occurring before any blocked write lock operation), that is, the read lock can be held by multiple data readers simultaneously. When the count maintained by the read lock is cleared to zero, the read lock returns to the unlocked state. - In order to prevent the data writer from starving, when the read lock is in the locked state and there are blocked write lock operations, subsequent read lock operations will be blocked; in order to prevent the data reader from starving, when the write lock is in the locked state, after the write lock is unlocked, the previously blocked read lock operations will definitely succeed.
- The write lock of
- Usage Example
package main import ( "fmt" "time" "sync" ) func main() { var m sync.RWMutex go func() { m.RLock() fmt.Print("a") time.Sleep(time.Second) m.RUnlock() }() go func() { time.Sleep(time.Second * 1 / 4) m.Lock() fmt.Print("b") time.Sleep(time.Second) m.Unlock() }() go func() { time.Sleep(time.Second * 2 / 4) m.Lock() fmt.Print("c") m.Unlock() }() go func () { time.Sleep(time.Second * 3 / 4) m.RLock() fmt.Print("d") m.RUnlock() }() time.Sleep(time.Second * 3) fmt.Println() }
The above program is most likely to output abdc
, which is used to explain and verify the locking rules of the read-write lock. It should be noted that the use of the time.Sleep
call in the program for synchronization between goroutines should not be used in production code.
In practical applications, if read operations are frequent and write operations are few, the Mutex
can be replaced with an RWMutex
to improve execution efficiency. For example, replace the Mutex
in the above Counter
example with an RWMutex
:
... type Counter struct { //m sync.Mutex m sync.RWMutex n uint64 } func (c *Counter) Value() uint64 { //c.m.Lock() //defer c.m.Unlock() c.m.RLock() defer c.m.RUnlock() return c.n } ...
In addition, sync.Mutex
and sync.RWMutex
values can also be used to implement notifications, although this is not the most elegant implementation in Go. For example:
package main import ( "fmt" "sync" "time" ) func main() { var m sync.Mutex m.Lock() go func() { time.Sleep(time.Second) fmt.Println("Hi") m.Unlock() // Send a notification }() m.Lock() // Wait for the notification fmt.Println("Bye") }
In this example, a simple notification between goroutines is implemented through the Mutex
to ensure that "Hi" is printed before "Bye". For the memory ordering guarantees related to sync.Mutex
and sync.RWMutex
values, you can refer to the relevant documents on memory ordering guarantees in Go.
The types in the sync
standard library package play a crucial role in the concurrent programming of the Go language. Developers need to reasonably select and correctly use these synchronization types according to specific business scenarios and requirements, so as to write efficient, reliable and thread-safe concurrent programs. At the same time, when writing concurrent code, it is also necessary to have an in-depth understanding of various concepts and potential problems in concurrent programming, such as data races, deadlocks, etc., and ensure the correctness and stability of the program in a concurrent environment through sufficient testing and verification.
Leapcell: The Best of Serverless Web Hosting
Finally, I would like to recommend a platform that is most suitable for deploying Go services: Leapcell
🚀 Build with Your Favorite Language
Develop effortlessly in JavaScript, Python, Go, or Rust.
🌍 Deploy Unlimited Projects for Free
Only pay for what you use—no requests, no charges.
⚡ Pay-as-You-Go, No Hidden Costs
No idle fees, just seamless scalability.
🔹 Follow us on Twitter: @LeapcellHQ