Prompt
When sharing data across goroutines, always use proper synchronization mechanisms to prevent race conditions. Race conditions are difficult to debug and can cause intermittent failures that only appear under load.
Key practices:
- Always protect mutex unlocking with defer:
func() { mu.Lock() defer mu.Unlock() // Code that might panic or return early }() - Clone data structures before sharing across goroutines:
// When parallelism > 1, clone to prevent concurrent modifications if parallelism > 1 { resp = resp.CloneVT() } - Use atomic operations for simple flags:
var rowAdded atomic.Bool // Later in code executed by multiple goroutines if len(rresult.Rows) > 0 { rowAdded.Store(true) } // Check safely if rowAdded.Load() { // ... } - Return copies of slices to prevent external mutation:
func (ml *MemoryLogger) LogEvents() []*logutilpb.Event { ml.mu.Lock() defer ml.mu.Unlock() return slices.Clone(ml.Events) } - Always clean up resources with defer:
ctx, cancel := context.WithCancel(ctx) defer cancel() // Prevents context leaks - Consider message queues for channels with unbounded consumers:
// For high-throughput messaging where backpressure is a concern mq := concurrency.NewMessageQueue[*TabletHealth]() go func() { for { th, validRes := mq.Receive() if validRes { c <- th } } }() - Use
chan struct{}overchan boolfor signaling:// More efficient and idiomatic for simple signaling semAcquiredChan := make(chan struct{}) // When signaling semAcquiredChan <- struct{}{}
Run tests with the -race flag to catch race conditions during development.