Prompt
When working with concurrent code, carefully manage shared resources and synchronization to prevent race conditions, deadlocks, and resource leaks:
- Protect shared data: Always synchronize access to data accessed by multiple goroutines using appropriate primitives like mutexes or channels.
// BAD: Accessing map from multiple goroutines without synchronization
go func() {
runErrs[testServer.id] = err // Race condition!
}()
// GOOD: Use mutex to protect shared map
var mu sync.Mutex
go func() {
mu.Lock()
defer mu.Unlock()
runErrs[testServer.id] = err
}()
- Deep copy shared objects: When passing data to goroutines, make defensive copies to prevent concurrent modification.
// BAD: Modifying the same object across goroutines
go func() {
// Both goroutines modify the same object
created.Status = "processing"
}()
// GOOD: Create a copy for the goroutine to modify
go func() {
createdCopy := created.DeepCopyObject()
createdCopy.Status = "processing"
}()
- Resource cleanup: Use
deferto ensure resources are always cleaned up, even in error paths.
// BAD: Channel might not be closed on some error paths
if err != nil {
return // events channel never closed!
}
close(events)
// GOOD: Guarantee cleanup with defer
defer close(events)
- Context propagation: Properly handle context cancellation in goroutines to allow graceful shutdown.
// BAD: Using Background context disconnects from parent cancellation
go func() {
ctx := context.Background() // Ignores parent cancellation signals
// long-running operation that won't be cancelled
}()
// GOOD: Propagate the original context
go func() {
// This operation will be cancelled when parent context is cancelled
select {
case <-ctx.Done():
return
case <-time.After(5 * time.Second):
// continue processing
}
}()
- Channel communication: Be aware of ordering and blocking issues when sending from multiple goroutines to a single channel.
// BAD: Multiple goroutines racing to send to channel can cause ordering issues
go func() {
stream <- event // May race with other senders
}()
// GOOD: Use a central goroutine to manage channel sends
eventQueue := make(chan Event, 100)
go func() {
for event := range eventQueue {
stream <- event // Maintains ordering
}
}()
- Signaling completion: Use channel close for signaling instead of sending values.
// BAD: Sending value for signaling
st.stateRunnerShutdown <- struct{}{}
// GOOD: Closing channel for signaling
close(st.stateRunnerShutdown)