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.