sync.Mutex & sync.RWMutex
Part of the Object Oriented Programming section of Coddy's GO journey — lesson 63 of 107.
While channels are Go's preferred way to coordinate goroutines, sometimes you need to protect shared data directly. The sync package provides mutexes—locks that ensure only one goroutine accesses a resource at a time.
A sync.Mutex has two methods: Lock() and Unlock(). When a goroutine calls Lock(), it gains exclusive access. Other goroutines calling Lock() will block until Unlock() is called:
type Counter struct {
mu sync.Mutex
value int
}
func (c *Counter) Increment() {
c.mu.Lock()
c.value++
c.mu.Unlock()
}
func (c *Counter) Value() int {
c.mu.Lock()
defer c.mu.Unlock()
return c.value
}Using defer c.mu.Unlock() ensures the lock is released even if the function returns early or panics—a common and recommended pattern.
When reads are frequent but writes are rare, sync.RWMutex offers better performance. It allows multiple readers simultaneously, but writers get exclusive access:
type Cache struct {
mu sync.RWMutex
data map[string]string
}
func (c *Cache) Get(key string) string {
c.mu.RLock() // multiple readers allowed
defer c.mu.RUnlock()
return c.data[key]
}
func (c *Cache) Set(key, value string) {
c.mu.Lock() // exclusive access for writing
defer c.mu.Unlock()
c.data[key] = value
}Use RLock()/RUnlock() for read operations and Lock()/Unlock() for writes. This allows concurrent reads while ensuring writes are safe.
Challenge
EasyLet's build a thread-safe inventory system that tracks product stock levels. Your system will handle concurrent reads and writes safely using mutexes, ensuring data integrity when multiple operations happen simultaneously.
You'll organize your code across two files:
inventory.go: Define your thread-safe inventory management system.Create an
Inventorystruct that stores product quantities in a map and uses ansync.RWMutexto protect access. Your inventory should support these operations:NewInventory() *Inventory- Creates a new inventory with an initialized mapAddStock(product string, quantity int)- Adds quantity to a product's stock (use exclusive lock since this modifies data)GetStock(product string) int- Returns the current stock for a product, or 0 if not found (use read lock since this only reads data)RemoveStock(product string, quantity int) bool- Removes quantity from stock if sufficient stock exists. Returnstrueif successful,falseif insufficient stock (use exclusive lock)
Remember to use
deferfor unlocking to ensure locks are always released properly.main.go: Read operations and demonstrate your thread-safe inventory.Read the number of operations, then process each operation. Each operation has a type (
add,get, orremove), a product name, and foradd/removeoperations, a quantity.For each operation, print the result:
add: PrintAdded [quantity] [product]get: Print[product]: [stock] in stockremove: PrintRemoved [quantity] [product]if successful, orInsufficient stock for [product]if not
The following inputs will be provided:
- Line 1: Number of operations (integer)
- Following lines: For each operation:
- Operation type (
add,get, orremove) - Product name
- Quantity (only for
addandremoveoperations)
- Operation type (
For example, given:
5
add
apples
50
get
apples
remove
apples
30
remove
apples
25
get
applesYour output should be:
Added 50 apples
apples: 50 in stock
Removed 30 apples
Insufficient stock for apples
apples: 20 in stockThe RWMutex allows multiple GetStock calls to read simultaneously, while AddStock and RemoveStock get exclusive access when modifying the inventory.
Cheat sheet
The sync package provides mutexes to protect shared data when multiple goroutines need direct access to the same resource.
sync.Mutex
A sync.Mutex provides exclusive access using Lock() and Unlock():
type Counter struct {
mu sync.Mutex
value int
}
func (c *Counter) Increment() {
c.mu.Lock()
c.value++
c.mu.Unlock()
}
func (c *Counter) Value() int {
c.mu.Lock()
defer c.mu.Unlock()
return c.value
}Using defer with Unlock() ensures the lock is released even if the function returns early or panics.
sync.RWMutex
A sync.RWMutex allows multiple concurrent readers but exclusive access for writers:
type Cache struct {
mu sync.RWMutex
data map[string]string
}
func (c *Cache) Get(key string) string {
c.mu.RLock() // multiple readers allowed
defer c.mu.RUnlock()
return c.data[key]
}
func (c *Cache) Set(key, value string) {
c.mu.Lock() // exclusive access for writing
defer c.mu.Unlock()
c.data[key] = value
}Use RLock()/RUnlock() for read operations and Lock()/Unlock() for write operations.
Try it yourself
package main
import (
"bufio"
"fmt"
"os"
"strconv"
"strings"
)
func main() {
reader := bufio.NewReader(os.Stdin)
// Read number of operations
line, _ := reader.ReadString('\n')
numOps, _ := strconv.Atoi(strings.TrimSpace(line))
// Create a new inventory
inventory := NewInventory()
// Process each operation
for i := 0; i < numOps; i++ {
// Read operation type
opLine, _ := reader.ReadString('\n')
opType := strings.TrimSpace(opLine)
// Read product name
productLine, _ := reader.ReadString('\n')
product := strings.TrimSpace(productLine)
// TODO: Handle each operation type (add, get, remove)
// For "add" and "remove", read the quantity from input
// Call the appropriate inventory method
// Print the result according to the challenge description
switch opType {
case "add":
// TODO: Read quantity, add stock, print result
case "get":
// TODO: Get stock, print result
case "remove":
// TODO: Read quantity, remove stock, print appropriate result
}
}
}
This lesson includes a short quiz. Start the lesson to answer it and track your progress.
All lessons in Object Oriented Programming
1Fundamentals of Go OOP
External FilesGo Workspace & ModulesPackages & ImportsExported vs Unexported NamesIntroduction to OOP in GoStructs as ClassesDefining Methods on StructsPointer vs Value ReceiversStruct InitializationConstructor FunctionsRecap - Simple Calculator4Interfaces
Introduction to InterfacesImplicit ImplementationInterface as ContractEmpty Interface (any)Type AssertionType SwitchInterface CompositionStringer & Error InterfacesRecap - Shape Calculator7Encapsulation
Exported vs Unexported FieldsPackage-Level EncapsulationGetter & Setter MethodsInformation Hiding in GoRecap - Student Records10Generics (Go 1.18+)
Introduction to GenericsType ParametersType ConstraintsGeneric StructsGeneric Methods WorkaroundRecap - Generic Collection2Types & Structs Deep Dive
Basic & Composite TypesCustom Type DefinitionsStruct TagsAnonymous StructsNested StructsZero Values & DefaultsRecap - Contact Book5Composition Over Inheritance
Why Go Has No InheritanceStruct Embedding BasicsMethod PromotionEmbedding Multiple StructsEmbedding vs AggregationShadowing Embedded MethodsRecap - Employee Hierarchy8Error Handling & OOP
The error InterfaceCustom Error TypesError Wrapping (fmt.Errorf)Sentinel Errorserrors.Is() and errors.As()Panic, Defer, and RecoverRecap - File Parser3Pointers & Memory
Pointer Basics in GoPointers to StructsPass by Value vs ReferenceThe new() FunctionGarbage Collection in GoRecap - Linked List Builder6Polymorphism in Go
Polymorphism via InterfacesDuck Typing in GoInterface Satisfaction RulesPolymorphic CollectionsDependency InjectionRecap - Payment Processor9Concurrency & OOP
Goroutines BasicsChannels & CommunicationBuffered vs Unbuffered ChanSelect Statementsync.Mutex & sync.RWMutexsync.WaitGroupThread-Safe Struct DesignRecap - Worker Pool