A Pattern for Synchronization Primitives in Complex Go Structures

It’s common in Go to have a struct that contains many pieces of state, some of which need protecting by different synchronization primitives. For example, in HashiCorp’s Raft library, the Raft structure contains several such examples:

type Raft struct {
    // Note that most fields are omitted...

	lastContact     time.Time
	lastContactLock sync.RWMutex

	leaderAddr ServerAddress
	leaderID   ServerID
	leaderLock sync.RWMutex

	observersLock sync.RWMutex
	observers     map[uint64]*Observer
}

For the most part, a developer is expected to work out which fields are protected by which synchronization primitives - perhaps by reading the comments, perhaps from a stated design document - but often by guesswork.

While it’s possible that Go will one day (like Rust) have generic versions of sync.Mutex and sync.RWMutex which prevent such misuse, there is a useful pattern that I’ve used in the past, and that is used extensively in wireguard-go - use a nested struct with an embedded field which is the synchronization primitive and the fields it is supposed to protect.

In this pattern, we’d refactor the above example to read like this:

type Raft struct {
    // Note that most fields are omitted...

    lastContact struct {
        sync.RWMutex

        time time.Time
    }

    leader struct {
        sync.RWMutex

        addr ServerAddress
        id   ServerID
    }

    observers struct {
        sync.RWMutex

        value map[uint64]*Observer
    }
}

Written like this, it’s obvious which fields are protected by which synchronization primitive, and the code reads nicely at call sites:

var raft Raft

func getLastContactTime() time.Time {
    raft.lastContact.RLock()
    defer raft.lastContact.RUnlock()

    return raft.lastContact.time
}

Or:

var raft Raft

func changeLeader(addr ServerAddress, id ServerID) {
    raft.leader.Lock()
    defer raft.leader.Unlock()

    raft.leader.addr = addr
    raft.leader.id = id
}

While this is obviously a trivial adjustment, it can make a big difference to how quickly someone unfamiliar with your codebase can apply the rules correctly.

go