Skip to content

Sync — WaitGroup, Mutex, and RWMutex

In JavaScript the event loop is single-threaded, so you almost never worry about concurrent writes to a shared variable (unless you are using SharedArrayBuffer with Worker Threads):

let count = 0;
// safe in Node.js — only one callback runs at a time
async function increment() { count++; }
await Promise.all([increment(), increment()]);
console.log(count); // always 2

In Go goroutines run on real OS threads. Two goroutines can read and write the same memory location at the same instant, producing a data race — which is undefined behavior:

// DANGEROUS — data race
count := 0
var wg sync.WaitGroup
for i := 0; i < 100; i++ {
wg.Add(1)
go func() {
defer wg.Done()
count++ // read-modify-write is NOT atomic
}()
}
wg.Wait()
// count may not equal 100

sync.Mutex ensures only one goroutine is inside a critical section at a time. Lock before entering, unlock before leaving (use defer so an early return never forgets to unlock):

TypeScript
// TypeScript: no mutex needed for normal async code
// With SharedArrayBuffer + Workers, use Atomics instead:
const sab = new SharedArrayBuffer(4);
const view = new Int32Array(sab);
Atomics.add(view, 0, 1); // atomic increment
Go
// Go: sync.Mutex protects the critical section
package main
import (
"fmt"
"sync"
)
type SafeCounter struct {
mu sync.Mutex
count int
}
func (c *SafeCounter) Increment() {
c.mu.Lock() // acquire lock
defer c.mu.Unlock() // release on return — always
c.count++
}
func main() {
var wg sync.WaitGroup
counter := &SafeCounter{}
for i := 0; i < 100; i++ {
wg.Add(1)
go func() {
defer wg.Done()
counter.Increment()
}()
}
wg.Wait()
fmt.Println("Final count:", counter.count) // always 100
}

RWMutex — multiple readers, exclusive writer

Section titled “RWMutex — multiple readers, exclusive writer”

When data is read frequently but written rarely, sync.RWMutex delivers better throughput than a plain Mutex. Multiple goroutines can hold a read lock simultaneously; a write lock is exclusive:

TypeScript
// TypeScript: no built-in RWMutex concept
// In Worker Threads you use message-passing instead of shared locks
Go
// Go: RWMutex — concurrent reads, exclusive writes
package main
import (
"fmt"
"sync"
)
type Cache struct {
mu sync.RWMutex
data map[string]string
}
func (c *Cache) Get(key string) string {
c.mu.RLock() // many goroutines can hold RLock at once
defer c.mu.RUnlock()
return c.data[key]
}
func (c *Cache) Set(key, value string) {
c.mu.Lock() // exclusive — all readers must finish first
defer c.mu.Unlock()
c.data[key] = value
}
func main() {
cache := &Cache{data: make(map[string]string)}
cache.Set("lang", "Go")
fmt.Println(cache.Get("lang")) // Go
}
package main
import (
"fmt"
"sync"
)
type SafeCounter struct {
mu sync.Mutex
count int
}
func (c *SafeCounter) Increment() {
c.mu.Lock()
defer c.mu.Unlock()
c.count++
}
func (c *SafeCounter) Value() int {
c.mu.Lock()
defer c.mu.Unlock()
return c.count
}
func main() {
var wg sync.WaitGroup
counter := &SafeCounter{}
for i := 0; i < 100; i++ {
wg.Add(1)
go func() {
defer wg.Done()
counter.Increment()
}()
}
wg.Wait()
fmt.Println("Final count:", counter.Value())
}
Why do data races occur in Go but not in typical JavaScript code?
How does RWMutex differ from a plain Mutex?
Which flag enables Go's built-in race detector?