Make goroutines safely share memory

Use the Mutex and RWMutex types from the golang.org/pkg/sync package to lock access to a given variable.

If multiple goroutines write to a variable, or do writing operations on a datatype (ex mutating a map or a slice), the data might get corrupt and in most cases it will just result in some panic errors from the Go runtime.

Lock with Mutex

This builds on the sample from Make goroutines wait for each other.

import (
    "fmt"
    "sync"
)

totalSum := 0

func AddToTotalSum(delta int, m *sync.Mutex) {
    m.Lock()
    defer m.Unlock()
    totalSum += sum
}

func SumSlice(slice []int, wg *sync.WaitGroup, m *sync.Mutex) {
    defer wg.Done()
    var sum int
    for _, num := range slice {
        sum += num
    }
    AddToTotalSum(sum, m)
}

func main() {
    slice := 
    wg := &sync.WaitGroup{}
    m := &sync.Mutex{}

    wg.Add(3)
    go PrintSum([]int{1, 2, 3, 4, 5}, wg, m)
    go PrintSum([]int{2, 4, 6, 8, 10}, wg, m)
    go PrintSum([]int{11, 22, 33, 44}, wg, m)
    wg.Wait()
    
    fmt.Printf("Total sum: %d\n", totalSum)
}

The Mutex.Lock() method will hault the goroutine if the lock is already being held by a different goroutine, until that lock is then released by Mutex.Unlock().

Lock with RWMutex

Reading data without mutating it is non-destructive and could therefore be done in parallel. Consider the following truthtable:

Parallel readingParallel writingOutcome
YesYesERR
NoYesERR
YesNoOK
NoNoNothing happens

Using a regular Mutex does not allow for this sole-reading use case. While this is not too common, the use case does come up here and there. For example in caches.

This is where the RWMutex comes in handy and can sometimes introduce a major performance boost where there is a lot of locking going on.

The RWMutex allows parallel reading, but as long as there is any writing involved it will start acting as a regular Mutex and hault the goroutines until the resource is unlocked.

import (
    "fmt"
    "sync"
)

usernameCache := map[int]string

func FetchFromCache(id int, m *sync.RWMutex) (string, bool) {
    m.RLock()
    defer m.RUnlock()
    name, ok := usernameCache[id]
    return name, ok
}

func FetchFromDB(id int, m *sync.RWMutex) (string, bool) {
    if name, err := db.fetchUsernameByID(id); err != nil {
        // Store it in cache
        m.Lock()
        defer m.Unlock()
        usernameCache[id] = name
        // then return
        return name, true
    }
    return "", false
}

func PrintUsername(id int, wg *sync.WaitGroup, m *sync.Mutex) {
    defer wg.Done()

    name, ok = FetchFromCache(id, m)
    if !ok {
        name, ok = FetchFromDB(id, m)
        if !ok {
            fmt.Printf("Unable to find user by ID: %d\n", id)
        } else {
            fmt.Printf("Found user by ID from DB: %d: %q\n", id, name)
        }
    } else {
        fmt.Printf("Found user by ID from cache: %d: %q\n", id, name)
    }
}

func main() {
    wg := &sync.WaitGroup{}
    m := &sync.Mutex{}

    wg.Add(3)
    go PrintUsername(1, wg, m)
    go PrintUsername(2, wg, m)
    go PrintUsername(123, wg, m)
    wg.Wait()
}

Keep in mind that using RWMutex with just regular .Lock() and .Unlock() is has a slight bigger performance hit than using the basic Mutex. Only use RWMutex when you can take advantage of it.

References