Go/
Lesson

Go's motto is "share memory by communicating," but sometimes you genuinely need shared state. A cache, a counter, a connection poolWhat is connection pool?A set of pre-opened database connections that your app reuses instead of opening and closing a new one for every request., these are naturally shared resources. The sync package provides the low-level primitives for safe concurrent access. AI reaches for these reflexively because they look familiar from other languages, but often uses them where channels would be cleaner.

sync.MutexWhat is mutex?A mutual exclusion lock that prevents concurrent access to shared data - only one thread or goroutine can hold it at a time.

A mutex provides mutual exclusion, only one goroutineWhat is goroutine?A lightweight concurrent function in Go launched with the go keyword - much cheaper than an OS thread (~2KB vs ~1MB of stack). can hold the lock at a time:

type SafeCounter struct {
    mu    sync.Mutex
    count int
}

func (c *SafeCounter) Inc() {
    c.mu.Lock()
    defer c.mu.Unlock()
    c.count++
}

func (c *SafeCounter) Value() int {
    c.mu.Lock()
    defer c.mu.Unlock()
    return c.count
}

The pattern is always: Lock(), defer Unlock(), access the protected data, return. The defer ensures the unlock happens even if the code panics.

AI pitfall
AI sometimes generates Lock/Unlock without defer, using explicit Unlock() calls at each return point. This is fragile: add a new return path or early return and you forget to unlock, causing a deadlock. Always use defer.
// BAD - AI generates this
func (c *SafeCounter) ConditionalInc(shouldInc bool) int {
    c.mu.Lock()
    if !shouldInc {
        c.mu.Unlock()  // easy to forget one of these
        return c.count
    }
    c.count++
    c.mu.Unlock()
    return c.count
}

// CORRECT - defer handles all return paths
func (c *SafeCounter) ConditionalInc(shouldInc bool) int {
    c.mu.Lock()
    defer c.mu.Unlock()
    if shouldInc {
        c.count++
    }
    return c.count
}
02

The copy trap

This is the most insidious mutexWhat is mutex?A mutual exclusion lock that prevents concurrent access to shared data - only one thread or goroutine can hold it at a time. bug AI generates. Mutexes must never be copied, but Go's value semantics make this easy to do accidentally:

type Config struct {
    mu      sync.Mutex
    setting string
}

// WRONG - c is a copy, so c.mu is a copy - different lock!
func updateConfig(c Config) {
    c.mu.Lock()
    defer c.mu.Unlock()
    c.setting = "new"
}

// CORRECT - pointer receiver, same lock
func updateConfig(c *Config) {
    c.mu.Lock()
    defer c.mu.Unlock()
    c.setting = "new"
}
OperationWhat happens to the mutex
Pass struct by valueMutex is copied, new independent lock
Pass struct by pointerSame mutex, correct
Assign struct to new variableMutex is copied
Return struct from functionMutex is copied
Embed struct in another structSame mutex if accessed via pointer
AI pitfall
AI generates methods with value receivers on structs containing mutexes. func (c Config) Get() copies the Config, including its mutex. Use go vet, it catches this. But AI doesn't run go vet before giving you code.
03

sync.RWMutex

When reads vastly outnumber writes, RWMutex allows multiple concurrent readers:

type Cache struct {
    mu   sync.RWMutex
    data map[string]string
}

func (c *Cache) Get(key string) (string, bool) {
    c.mu.RLock()          // multiple goroutines can RLock simultaneously
    defer c.mu.RUnlock()
    val, ok := c.data[key]
    return val, ok
}

func (c *Cache) Set(key, val string) {
    c.mu.Lock()           // exclusive - blocks until all RLocks release
    defer c.mu.Unlock()
    c.data[key] = val
}
Lock typeConcurrent with RLock?Concurrent with Lock?Use for
RLock()YesNoRead-only access
Lock()NoNoWrite access
AI pitfall
AI uses RWMutex when there's a mix of reads and writes without analyzing the ratio. RWMutex has higher overhead than Mutex, it's only faster when reads outnumber writes by roughly 10:1 or more. For a 50/50 read/write ratio, plain Mutex is faster.
04

sync.Once

Guarantees a function executes exactly once, regardless of how many goroutines call it:

var (
    db   *Database
    once sync.Once
)

func GetDB() *Database {
    once.Do(func() {
        db = connectToDatabase() // runs exactly once
    })
    return db
}

Even if 100 goroutines call GetDB() simultaneously, connectToDatabase() runs once. All other callers block until it completes, then all return the same db instance.

AI pitfall
AI sometimes uses sync.Once to initialize a resource but doesn't handle initialization errors. If connectToDatabase() fails, once.Do still marks it as "done", subsequent calls return nil forever. You need either a sync.Once per attempt (not how it works) or a different pattern like a mutex with a flag.
// BAD - if init fails, it's failed forever
once.Do(func() {
    db, err = connect()
    // err is non-nil but once.Do won't retry
})

// BETTER - use mutex to allow retry
func GetDB() (*Database, error) {
    mu.Lock()
    defer mu.Unlock()
    if db != nil {
        return db, nil
    }
    var err error
    db, err = connect()
    return db, err
}
05

sync.Map

A concurrent-safe map. But it's not a general-purpose replacement for map + Mutex:

var cache sync.Map

cache.Store("key", "value")

if val, ok := cache.Load("key"); ok {
    fmt.Println(val.(string))
}

// LoadOrStore: atomic check-and-set
actual, loaded := cache.LoadOrStore("key", "default")
Featuremap + Mutexsync.Map
Type safetyYes (generics)No (any types, needs assertion)
General performanceBetter in most casesWorse
Many reads, few writesGoodOptimized for this
Disjoint key accessSame performanceOptimized for this
When to useDefault choiceOnly when benchmarks prove it's faster
AI pitfall
AI loves sync.Map because it looks simpler than a mutex-protected map. But it loses type safety, has worse performance for most workloads, and its API is clunky. Use map + Mutex by default. Only switch to sync.Map if profiling shows contention.
06

Channels vs mutexes: decision framework

This is the most common design question in concurrent Go, and where AI makes the worst architectural choices:

SituationUse channelsUse mutex
Passing data ownership between goroutinesYesNo
Coordinating goroutine lifecycleYesNo
Protecting a shared data structureNoYes
Simple counter or flagNoYes
Pipeline of transformationsYesNo
Cache with concurrent readersNoYes (RWMutex)
Fan-out work distributionYesNo
Rate limitingEither worksEither works

The mental model: channels transfer ownership, mutexes protect shared state. If you're sending data from one goroutineWhat is goroutine?A lightweight concurrent function in Go launched with the go keyword - much cheaper than an OS thread (~2KB vs ~1MB of stack). to another, use a channelWhat is channel?A typed conduit in Go used to pass values between goroutines - can be unbuffered (synchronous) or buffered (async queue).. If multiple goroutines need to read/write the same variable, use a mutexWhat is mutex?A mutual exclusion lock that prevents concurrent access to shared data - only one thread or goroutine can hold it at a time..

AI pitfall
AI trained on Java/C++ code defaults to mutexes everywhere. It'll wrap every shared variable in a mutex when a channel-based design would be cleaner and less error-prone. When reviewing AI code, ask: "Is this protecting shared state, or is it coordinating between goroutines?" If the latter, suggest channels.
07

Deadlocks from lock ordering

When two goroutines acquire multiple locks in different orders, deadlockWhat is deadlock?A situation where two or more operations are stuck waiting on each other forever, so none of them can proceed.:

// DEADLOCK - inconsistent lock ordering
// Goroutine 1          Goroutine 2
// mu1.Lock()           mu2.Lock()
// mu2.Lock() <- waits  mu1.Lock() <- waits

// FIX - always acquire in the same order
func transfer(from, to *Account, amount int) {
    // Ensure consistent ordering by comparing addresses
    first, second := from, to
    if uintptr(unsafe.Pointer(from)) > uintptr(unsafe.Pointer(to)) {
        first, second = to, from
    }
    first.mu.Lock()
    defer first.mu.Unlock()
    second.mu.Lock()
    defer second.mu.Unlock()

    from.balance -= amount
    to.balance += amount
}
AI pitfall
AI generates code that acquires multiple locks without establishing a consistent order. This works in testing (race conditions need specific timing to trigger) but deadlocks in production under load.