[teamai] Push 87 resource(s) from XingfenD

This commit is contained in:
2026-09-10 16:10:45 +08:00
parent 425c9c078a
commit 65c04def51
1314 changed files with 211681 additions and 0 deletions
@@ -0,0 +1,160 @@
# Channels and Select Patterns
## Goroutine Lifecycle
NEVER start a goroutine without knowing how it stops. Every goroutine MUST answer: **how will it stop?**
```go
// ✗ Bad — fire-and-forget, no way to stop or wait
func startWorker() {
go func() {
for {
doWork() // runs forever, leaks on shutdown
}
}()
}
// ✓ Good — goroutine respects context cancellation, caller can wait
func startWorker(ctx context.Context) *sync.WaitGroup {
var wg sync.WaitGroup
wg.Add(1)
go func() {
defer wg.Done()
for {
select {
case <-ctx.Done():
return
default:
doWork(ctx)
}
}
}()
return &wg
}
```
### Panic Recovery at Goroutine Boundaries
A panic in a goroutine crashes the entire process. Always recover at goroutine boundaries in production code:
```go
go func() {
defer func() {
if r := recover(); r != nil {
// ...
}
}()
doWork(ctx)
}()
```
## Channel Direction
Specify direction in function signatures to prevent misuse at compile time:
```go
// ✗ Bad — caller could accidentally close or send on a receive-only channel
func consume(ch chan int) { ... }
// ✓ Good — compiler enforces correct usage
func produce(ch chan<- int) { ... } // send-only
func consume(ch <-chan int) { ... } // receive-only
```
## Channel Closing
Channels MUST be closed by the sender (producer), NEVER by the receiver — it causes a panic if the sender writes after close.
```go
// ✓ Good — producer closes when done
func generate(ctx context.Context) <-chan int {
ch := make(chan int)
go func() {
defer close(ch) // sender closes
for i := 0; ; i++ {
select {
case ch <- i:
case <-ctx.Done():
return
}
}
}()
return ch
}
```
## Buffer Size
| Size | When to use |
| --- | --- |
| 0 (unbuffered) | Default. Synchronizes sender and receiver — use when you need handoff guarantees |
| 1 | Signal channels (`done := make(chan struct{}, 1)`), or when sender must not block on a single pending item |
| N > 1 | Only with measured justification — document why N was chosen and what happens when the buffer fills |
```go
// ✓ Good — unbuffered for synchronous handoff
ch := make(chan Result)
// ✓ Good — buffered 1 for signal
done := make(chan struct{}, 1)
// ✗ Suspicious — arbitrary large buffer hides backpressure problems
// Give explanation in comments.
ch := make(chan Task, 1000) // why 1000? what if it fills?
```
## Select for Non-Blocking Communication
Use `select` to multiplex channel operations and always include `ctx.Done()` to prevent goroutine leaks:
```go
func process(ctx context.Context, in <-chan Task, out chan<- Result) {
for {
select {
case <-ctx.Done():
return
case task, ok := <-in:
if !ok {
return // channel closed
}
result := handle(ctx, task)
select {
case out <- result:
case <-ctx.Done():
return
}
}
}
}
```
## Avoid Repeated `time.After` in Hot Loops
```go
// ✗ Bad — creates a new timer on every iteration
for {
select {
case msg := <-ch:
handle(msg)
case <-time.After(5 * time.Second): // repeated allocation/churn
handleTimeout()
}
}
// ✓ Good (Go 1.23+) — reuse the timer
timer := time.NewTimer(5 * time.Second)
defer timer.Stop()
for {
select {
case msg := <-ch:
timer.Stop()
timer.Reset(5 * time.Second)
handle(msg)
case <-timer.C:
handleTimeout()
timer.Reset(5 * time.Second)
}
}
```
For Go <1.23, if `timer.Stop()` returns false, drain a possible stale value before `Reset`. In Go 1.23+, receiving from `timer.C` after `Stop` returns is guaranteed to block rather than receive a stale value.
@@ -0,0 +1,263 @@
# Pipelines and Worker Pools
## Pipeline Pattern
A pipeline is a series of stages connected by channels, where each stage is a goroutine (or group of goroutines) that:
1. Receives values from an upstream channel
2. Processes each value
3. Sends results to a downstream channel
```go
// Stage 1: Generate integers
func generate(ctx context.Context, nums ...int) <-chan int {
out := make(chan int)
go func() {
defer close(out)
for _, n := range nums {
select {
case out <- n:
case <-ctx.Done():
return
}
}
}()
return out
}
// Stage 2: Square each integer
func square(ctx context.Context, in <-chan int) <-chan int {
out := make(chan int)
go func() {
defer close(out)
for n := range in {
select {
case out <- n * n:
case <-ctx.Done():
return
}
}
}()
return out
}
// Usage
func main() {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
ch := generate(ctx, 2, 3, 4)
results := square(ctx, ch)
for v := range results {
fmt.Println(v) // 4, 9, 16
}
}
```
**Key rules for pipelines**:
- Pipeline stages MUST accept and respect context cancellation — every stage must select on `ctx.Done()` to avoid goroutine leaks on early cancellation
- The producer (first stage) closes its output channel; each subsequent stage closes its own output
- NEVER create unbounded goroutines in pipeline stages
- Use unbuffered channels unless you have measured throughput needs
## Fan-Out / Fan-In
**Fan-out**: multiple goroutines read from the same channel to parallelize CPU-bound work. **Fan-in**: multiple channels are merged into a single output channel.
```go
// Fan-out: N workers reading from the same input channel
func fanOut(ctx context.Context, in <-chan Task, workers int) <-chan Result {
out := make(chan Result)
var wg sync.WaitGroup
for i := 0; i < workers; i++ {
wg.Add(1)
go func() {
defer wg.Done()
for {
select {
case task, ok := <-in:
if !ok {
return
}
select {
case out <- process(ctx, task):
case <-ctx.Done():
return
}
case <-ctx.Done():
return
}
}
}()
}
go func() {
wg.Wait()
close(out)
}()
return out
}
```
```go
// Fan-in: merge multiple channels into one
func fanIn(ctx context.Context, channels ...<-chan Result) <-chan Result {
out := make(chan Result)
var wg sync.WaitGroup
for _, ch := range channels {
wg.Add(1)
go func(c <-chan Result) {
defer wg.Done()
for v := range c {
select {
case out <- v:
case <-ctx.Done():
return
}
}
}(ch)
}
go func() {
wg.Wait()
close(out)
}()
return out
}
```
## Worker Pool with errgroup
Fan-out workers SHOULD use `errgroup.SetLimit` for bounded concurrency. For most use cases, `errgroup.SetLimit` replaces hand-rolled worker pools:
```go
func processAll(ctx context.Context, tasks []Task) error {
g, ctx := errgroup.WithContext(ctx)
g.SetLimit(10) // max 10 concurrent workers
for _, task := range tasks {
g.Go(func() error {
return process(ctx, task)
})
}
return g.Wait()
}
```
Use a hand-rolled worker pool only when you need:
- Per-worker state (connections, buffers)
- Custom backpressure or priority scheduling
- Graceful draining with in-flight task completion
## Bounded Concurrency with Semaphore
When you need fine-grained concurrency control without errgroup:
```go
func processAll(ctx context.Context, items []Item) error {
sem := make(chan struct{}, 10) // semaphore of 10
var wg sync.WaitGroup
for _, item := range items {
wg.Add(1)
sem <- struct{}{} // acquire
go func(item Item) {
defer wg.Done()
defer func() { <-sem }() // release
process(ctx, item)
}(item)
}
wg.Wait()
return nil
}
```
Prefer `errgroup.SetLimit` over this pattern when error propagation is needed.
## Pipeline Alternatives
### Go 1.23+ Iterators (range-over-func)
For in-process data transformations that do not need concurrency, iterators avoid the overhead of goroutines and channels:
```go
func Filter[T any](seq iter.Seq[T], pred func(T) bool) iter.Seq[T] {
return func(yield func(T) bool) {
for v := range seq {
if pred(v) {
if !yield(v) {
return
}
}
}
}
}
func Map[T, U any](seq iter.Seq[T], f func(T) U) iter.Seq[U] {
return func(yield func(U) bool) {
for v := range seq {
if !yield(f(v)) {
return
}
}
}
}
```
Use iterators when:
- Processing is CPU-bound and does not benefit from parallelism
- You want lazy evaluation without goroutine overhead
- The data source is already sequential (slice, database cursor)
Use goroutine+channel pipelines when:
- Stages involve I/O (network, disk) that benefits from concurrency
- You need true parallelism across CPU cores
- Stages have different throughput characteristics
### samber/ro
`samber/ro` provides a fluent, type-safe pipeline API for read-only collections:
```go
import "github.com/samber/ro"
emails, _ := ro.Collect( // ignore error
ro.Pipe(
ro.FromSlice(users),
ro.Filter(func(u User) bool { return u.Active }),
ro.Map(func(u User) string { return u.Email }),
),
)
```
Use `samber/ro` for sequential data transformations that benefit from a fluent API. It might also support parallel processing if needed.
## Goroutine Leak Detection
Goroutine leaks SHOULD be detected with goleak in tests. Use `go.uber.org/goleak` in `TestMain` to catch leaked goroutines across all tests:
```go
func TestMain(m *testing.M) {
goleak.VerifyTestMain(m)
}
```
## Common Pipeline Mistakes
| Mistake | Fix |
| --- | --- |
| Missing `ctx.Done()` in pipeline stage | Always select on context to allow cancellation |
| Not closing output channel | Producer must `defer close(out)` |
| Unbounded goroutine spawning | Use `errgroup.SetLimit` or a semaphore |
| Sending mutable data through channel | Send copies or immutable values |
| Blocking send without select | Wrap channel sends in select with `ctx.Done()` |
→ See `samber/cc-skills-golang@golang-concurrency` skill for sync primitives and channel patterns.
@@ -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.