[teamai] Push 87 resource(s) from XingfenD
This commit is contained in:
@@ -0,0 +1,326 @@
|
||||
# Sync Primitives Deep Dive
|
||||
|
||||
## sync.Mutex
|
||||
|
||||
Protects shared state with exclusive access. MUST hold the lock for the shortest time possible — NEVER hold a mutex across I/O, network calls, or channel operations.
|
||||
|
||||
```go
|
||||
type SafeCache struct {
|
||||
mu sync.Mutex
|
||||
items map[string]string
|
||||
}
|
||||
|
||||
func (c *SafeCache) Get(key string) (string, bool) {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
v, ok := c.items[key]
|
||||
return v, ok
|
||||
}
|
||||
|
||||
func (c *SafeCache) Set(key, value string) {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
c.items[key] = value
|
||||
}
|
||||
```
|
||||
|
||||
### Embedding Convention
|
||||
|
||||
Embed the mutex as an unexported field, placed directly above the fields it protects:
|
||||
|
||||
```go
|
||||
type Registry struct {
|
||||
mu sync.Mutex // protects entries
|
||||
entries map[string]Entry
|
||||
}
|
||||
```
|
||||
|
||||
## sync.RWMutex
|
||||
|
||||
SHOULD be used when reads greatly outnumber writes. Multiple goroutines can hold `RLock` simultaneously; `Lock` is exclusive.
|
||||
|
||||
```go
|
||||
type Config struct {
|
||||
mu sync.RWMutex
|
||||
values map[string]string
|
||||
}
|
||||
|
||||
func (c *Config) Get(key string) string {
|
||||
c.mu.RLock()
|
||||
defer c.mu.RUnlock()
|
||||
return c.values[key]
|
||||
}
|
||||
|
||||
func (c *Config) Set(key, value string) {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
c.values[key] = value
|
||||
}
|
||||
```
|
||||
|
||||
**Pitfall**: Do not upgrade RLock to Lock — this deadlocks. Release RLock first, then acquire Lock.
|
||||
|
||||
## sync/atomic
|
||||
|
||||
Lock-free operations for simple values. SHOULD be preferred over Mutex for simple counter operations. Faster than mutex for low-contention counters and flags.
|
||||
|
||||
```go
|
||||
// ✓ Good — atomic for a simple counter
|
||||
var requestCount atomic.Int64
|
||||
|
||||
func handleRequest() {
|
||||
requestCount.Add(1)
|
||||
}
|
||||
|
||||
func getCount() int64 {
|
||||
return requestCount.Load()
|
||||
}
|
||||
```
|
||||
|
||||
```go
|
||||
// ✓ Good — atomic.Bool for a shutdown flag
|
||||
var shuttingDown atomic.Bool
|
||||
|
||||
func shutdown() {
|
||||
shuttingDown.Store(true)
|
||||
}
|
||||
|
||||
func isRunning() bool {
|
||||
return !shuttingDown.Load()
|
||||
}
|
||||
```
|
||||
|
||||
Go 1.19+ provides typed atomics (`atomic.Int64`, `atomic.Bool`, `atomic.Pointer[T]`) — prefer these over raw `atomic.AddInt64`/`atomic.LoadInt64`.
|
||||
|
||||
## sync.Map
|
||||
|
||||
SHOULD only be used for write-once/read-many patterns. Optimized for two common patterns: (1) keys are written once and read many times, (2) multiple goroutines read/write disjoint key sets. For other patterns, a plain `map` + `sync.RWMutex` is faster.
|
||||
|
||||
```go
|
||||
var cache sync.Map
|
||||
|
||||
func Get(key string) (any, bool) {
|
||||
return cache.Load(key)
|
||||
}
|
||||
|
||||
func Set(key string, value any) {
|
||||
cache.Store(key, value)
|
||||
}
|
||||
|
||||
func GetOrSet(key string, compute func() any) any {
|
||||
if v, ok := cache.Load(key); ok {
|
||||
return v
|
||||
}
|
||||
v, _ := cache.LoadOrStore(key, compute())
|
||||
return v
|
||||
}
|
||||
```
|
||||
|
||||
**When NOT to use `sync.Map`**: when you need to iterate, get the length, or when writes are frequent and keys overlap heavily. Use `sync.RWMutex` + `map` instead.
|
||||
|
||||
## sync.Pool
|
||||
|
||||
Reuse temporary objects to reduce GC pressure. MUST NOT store pointers to stack-allocated objects. Objects in the pool may be reclaimed at any GC cycle — do not store persistent state.
|
||||
|
||||
```go
|
||||
var bufPool = sync.Pool{
|
||||
New: func() any {
|
||||
return new(bytes.Buffer)
|
||||
},
|
||||
}
|
||||
|
||||
func process(data []byte) string {
|
||||
buf := bufPool.Get().(*bytes.Buffer)
|
||||
defer func() {
|
||||
buf.Reset()
|
||||
bufPool.Put(buf)
|
||||
}()
|
||||
|
||||
buf.Write(data)
|
||||
// ... transform ...
|
||||
return buf.String()
|
||||
}
|
||||
```
|
||||
|
||||
**Rules**:
|
||||
|
||||
- Always `Reset()` before `Put()` — returning dirty objects causes bugs
|
||||
- Do not assume an object from `Get()` is zeroed — the `New` func only runs if the pool is empty
|
||||
- Best for short-lived, frequently allocated objects (buffers, encoders, temporary structs)
|
||||
|
||||
## sync.Once
|
||||
|
||||
MUST be used for one-time initialization. Execute exactly once, regardless of how many goroutines call it concurrently. Thread-safe by design.
|
||||
|
||||
```go
|
||||
type DBClient struct {
|
||||
initOnce sync.Once
|
||||
closeOnce sync.Once
|
||||
conn *sql.DB
|
||||
}
|
||||
|
||||
func (c *DBClient) getConn() *sql.DB {
|
||||
c.initOnce.Do(func() {
|
||||
var err error
|
||||
c.conn, err = sql.Open("postgres", dsn)
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("db init: %v", err))
|
||||
}
|
||||
})
|
||||
return c.conn
|
||||
}
|
||||
|
||||
func (c *DBClient) Close() error {
|
||||
var err error
|
||||
c.closeOnce.Do(func() {
|
||||
err = c.conn.Close()
|
||||
})
|
||||
return err
|
||||
}
|
||||
```
|
||||
|
||||
Go 1.21+ also provides `sync.OnceFunc`, `sync.OnceValue`, and `sync.OnceValues` for simpler use cases:
|
||||
|
||||
```go
|
||||
var loadConfig = sync.OnceValue(func() *Config {
|
||||
cfg, err := parseConfig("config.yaml")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return cfg
|
||||
})
|
||||
|
||||
// Usage: cfg := loadConfig()
|
||||
```
|
||||
|
||||
## sync.WaitGroup
|
||||
|
||||
Use `sync.WaitGroup` when you only need to wait for a set of goroutines to finish.
|
||||
|
||||
### Go 1.25+: `wg.Go`
|
||||
|
||||
`WaitGroup.Go` starts a goroutine, adds it to the group, and removes it from the group when the function returns.
|
||||
|
||||
```go
|
||||
func processAll(items []Item) {
|
||||
var wg sync.WaitGroup
|
||||
|
||||
for _, item := range items {
|
||||
// Go 1.22+ loop variables are per-iteration when the module has `go 1.22+`.
|
||||
// Do not add `item := item` solely for closure capture in modern modules.
|
||||
wg.Go(func() {
|
||||
process(item)
|
||||
})
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
}
|
||||
```
|
||||
|
||||
Rules:
|
||||
|
||||
- `WaitGroup.Go` is Go 1.25+, not Go 1.24.
|
||||
- The function passed to `wg.Go` must not panic.
|
||||
- `WaitGroup` does not propagate errors and does not cancel siblings.
|
||||
- For first-error-wins, cancellation, concurrency limits, or returned values, use `golang.org/x/sync/errgroup`.
|
||||
|
||||
**Benefits of `wg.Go()`**:
|
||||
|
||||
- No manual `Add`/`Done` bookkeeping
|
||||
- Lower risk of `Add`/`Wait` ordering bugs
|
||||
- Cleaner API for simple fire-and-wait work
|
||||
|
||||
**When to use**: Go 1.25+ projects for simple goroutines that must all finish, do not return errors, do not need cancellation, and must not panic. Use `errgroup` when work returns errors, needs cancellation, limits, or first-error behavior.
|
||||
|
||||
### Go <1.25 fallback
|
||||
|
||||
```go
|
||||
func processAll(ctx context.Context, items []Item) {
|
||||
var wg sync.WaitGroup
|
||||
for _, item := range items {
|
||||
wg.Add(1) // Add BEFORE go
|
||||
go func(item Item) {
|
||||
defer wg.Done()
|
||||
process(ctx, item)
|
||||
}(item)
|
||||
}
|
||||
wg.Wait() // blocks until all goroutines finish
|
||||
}
|
||||
```
|
||||
|
||||
```go
|
||||
// ✗ Bad — Add inside the goroutine (race: Wait may return before Add runs)
|
||||
go func() {
|
||||
wg.Add(1)
|
||||
defer wg.Done()
|
||||
process(item)
|
||||
}()
|
||||
```
|
||||
|
||||
## golang.org/x/sync/singleflight
|
||||
|
||||
Deduplicates concurrent calls for the same key. When multiple goroutines request the same resource simultaneously, only one executes; the rest wait and share the result.
|
||||
|
||||
```go
|
||||
var group singleflight.Group
|
||||
|
||||
func GetUser(ctx context.Context, id string) (*User, error) {
|
||||
v, err, _ := group.Do(id, func() (any, error) {
|
||||
// Only one goroutine executes this for a given id
|
||||
return db.QueryUser(ctx, id)
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return v.(*User), nil
|
||||
}
|
||||
```
|
||||
|
||||
**Use cases**: cache stampede prevention, deduplicating expensive lookups (DB, API), rate-limited external service calls.
|
||||
|
||||
## golang.org/x/sync/errgroup
|
||||
|
||||
Goroutine group with error propagation. Returns the first error from any goroutine. With `WithContext`, cancels remaining goroutines on first error.
|
||||
|
||||
```go
|
||||
func fetchAll(ctx context.Context, urls []string) ([]Response, error) {
|
||||
g, ctx := errgroup.WithContext(ctx) // cancel siblings on first error
|
||||
results := make([]Response, len(urls))
|
||||
|
||||
for i, url := range urls {
|
||||
g.Go(func() error {
|
||||
resp, err := fetch(ctx, url)
|
||||
if err != nil {
|
||||
return fmt.Errorf("fetching %s: %w", url, err)
|
||||
}
|
||||
results[i] = resp // safe: each goroutine writes to its own index
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
if err := g.Wait(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return results, nil
|
||||
}
|
||||
```
|
||||
|
||||
### Bounded Concurrency with SetLimit
|
||||
|
||||
SHOULD use `SetLimit` to bound concurrency and avoid unbounded goroutine spawning.
|
||||
|
||||
```go
|
||||
g, ctx := errgroup.WithContext(ctx)
|
||||
g.SetLimit(10) // at most 10 goroutines run concurrently
|
||||
|
||||
for _, task := range tasks {
|
||||
g.Go(func() error {
|
||||
return process(ctx, task)
|
||||
})
|
||||
}
|
||||
return g.Wait()
|
||||
```
|
||||
|
||||
This replaces hand-rolled worker pools for most use cases.
|
||||
|
||||
→ See `samber/cc-skills-golang@golang-concurrency` skill for high-level patterns and decision trees.
|
||||
Reference in New Issue
Block a user