devflow-kit 1.0.0 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +69 -0
- package/README.md +35 -11
- package/dist/cli.js +5 -1
- package/dist/commands/ambient.d.ts +18 -0
- package/dist/commands/ambient.js +136 -0
- package/dist/commands/init.d.ts +2 -0
- package/dist/commands/init.js +97 -10
- package/dist/commands/memory.d.ts +22 -0
- package/dist/commands/memory.js +175 -0
- package/dist/commands/uninstall.js +72 -5
- package/dist/plugins.js +74 -3
- package/dist/utils/post-install.d.ts +12 -0
- package/dist/utils/post-install.js +82 -1
- package/dist/utils/safe-delete-install.d.ts +7 -0
- package/dist/utils/safe-delete-install.js +40 -5
- package/package.json +2 -1
- package/plugins/devflow-accessibility/.claude-plugin/plugin.json +15 -0
- package/plugins/devflow-ambient/.claude-plugin/plugin.json +7 -0
- package/plugins/devflow-ambient/README.md +49 -0
- package/plugins/devflow-ambient/commands/ambient.md +110 -0
- package/plugins/devflow-ambient/skills/ambient-router/SKILL.md +89 -0
- package/plugins/devflow-ambient/skills/ambient-router/references/skill-catalog.md +68 -0
- package/plugins/devflow-audit-claude/.claude-plugin/plugin.json +1 -1
- package/plugins/devflow-code-review/.claude-plugin/plugin.json +1 -4
- package/plugins/devflow-code-review/agents/reviewer.md +8 -0
- package/plugins/devflow-code-review/commands/code-review-teams.md +11 -1
- package/plugins/devflow-code-review/commands/code-review.md +12 -2
- package/plugins/devflow-core-skills/.claude-plugin/plugin.json +3 -6
- package/plugins/devflow-core-skills/skills/docs-framework/SKILL.md +10 -6
- package/plugins/devflow-core-skills/skills/test-driven-development/SKILL.md +139 -0
- package/plugins/devflow-core-skills/skills/test-driven-development/references/rationalization-prevention.md +111 -0
- package/plugins/devflow-debug/.claude-plugin/plugin.json +1 -1
- package/plugins/devflow-frontend-design/.claude-plugin/plugin.json +15 -0
- package/plugins/devflow-go/.claude-plugin/plugin.json +15 -0
- package/plugins/devflow-go/skills/go/SKILL.md +187 -0
- package/plugins/devflow-go/skills/go/references/concurrency.md +312 -0
- package/plugins/devflow-go/skills/go/references/detection.md +129 -0
- package/plugins/devflow-go/skills/go/references/patterns.md +232 -0
- package/plugins/devflow-go/skills/go/references/violations.md +205 -0
- package/plugins/devflow-implement/.claude-plugin/plugin.json +1 -3
- package/plugins/devflow-implement/agents/coder.md +11 -6
- package/plugins/devflow-java/.claude-plugin/plugin.json +15 -0
- package/plugins/devflow-java/skills/java/SKILL.md +183 -0
- package/plugins/devflow-java/skills/java/references/detection.md +120 -0
- package/plugins/devflow-java/skills/java/references/modern-java.md +270 -0
- package/plugins/devflow-java/skills/java/references/patterns.md +235 -0
- package/plugins/devflow-java/skills/java/references/violations.md +213 -0
- package/plugins/devflow-python/.claude-plugin/plugin.json +15 -0
- package/plugins/devflow-python/skills/python/SKILL.md +188 -0
- package/plugins/devflow-python/skills/python/references/async.md +220 -0
- package/plugins/devflow-python/skills/python/references/detection.md +128 -0
- package/plugins/devflow-python/skills/python/references/patterns.md +226 -0
- package/plugins/devflow-python/skills/python/references/violations.md +204 -0
- package/plugins/devflow-react/.claude-plugin/plugin.json +15 -0
- package/plugins/{devflow-core-skills → devflow-react}/skills/react/SKILL.md +1 -1
- package/plugins/{devflow-core-skills → devflow-react}/skills/react/references/patterns.md +3 -3
- package/plugins/devflow-resolve/.claude-plugin/plugin.json +1 -1
- package/plugins/devflow-rust/.claude-plugin/plugin.json +15 -0
- package/plugins/devflow-rust/skills/rust/SKILL.md +193 -0
- package/plugins/devflow-rust/skills/rust/references/detection.md +131 -0
- package/plugins/devflow-rust/skills/rust/references/ownership.md +242 -0
- package/plugins/devflow-rust/skills/rust/references/patterns.md +210 -0
- package/plugins/devflow-rust/skills/rust/references/violations.md +191 -0
- package/plugins/devflow-self-review/.claude-plugin/plugin.json +1 -1
- package/plugins/devflow-specify/.claude-plugin/plugin.json +1 -1
- package/plugins/devflow-typescript/.claude-plugin/plugin.json +15 -0
- package/plugins/{devflow-core-skills → devflow-typescript}/skills/typescript/references/patterns.md +3 -3
- package/scripts/hooks/ambient-prompt.sh +48 -0
- package/scripts/hooks/background-memory-update.sh +49 -8
- package/scripts/hooks/ensure-memory-gitignore.sh +17 -0
- package/scripts/hooks/pre-compact-memory.sh +12 -6
- package/scripts/hooks/session-start-memory.sh +50 -8
- package/scripts/hooks/stop-update-memory.sh +10 -6
- package/shared/agents/coder.md +11 -6
- package/shared/agents/reviewer.md +8 -0
- package/shared/skills/ambient-router/SKILL.md +89 -0
- package/shared/skills/ambient-router/references/skill-catalog.md +68 -0
- package/shared/skills/docs-framework/SKILL.md +10 -6
- package/shared/skills/go/SKILL.md +187 -0
- package/shared/skills/go/references/concurrency.md +312 -0
- package/shared/skills/go/references/detection.md +129 -0
- package/shared/skills/go/references/patterns.md +232 -0
- package/shared/skills/go/references/violations.md +205 -0
- package/shared/skills/java/SKILL.md +183 -0
- package/shared/skills/java/references/detection.md +120 -0
- package/shared/skills/java/references/modern-java.md +270 -0
- package/shared/skills/java/references/patterns.md +235 -0
- package/shared/skills/java/references/violations.md +213 -0
- package/shared/skills/python/SKILL.md +188 -0
- package/shared/skills/python/references/async.md +220 -0
- package/shared/skills/python/references/detection.md +128 -0
- package/shared/skills/python/references/patterns.md +226 -0
- package/shared/skills/python/references/violations.md +204 -0
- package/shared/skills/react/SKILL.md +1 -1
- package/shared/skills/react/references/patterns.md +3 -3
- package/shared/skills/rust/SKILL.md +193 -0
- package/shared/skills/rust/references/detection.md +131 -0
- package/shared/skills/rust/references/ownership.md +242 -0
- package/shared/skills/rust/references/patterns.md +210 -0
- package/shared/skills/rust/references/violations.md +191 -0
- package/shared/skills/test-driven-development/SKILL.md +139 -0
- package/shared/skills/test-driven-development/references/rationalization-prevention.md +111 -0
- package/shared/skills/typescript/references/patterns.md +3 -3
- package/src/templates/managed-settings.json +14 -0
- package/plugins/devflow-code-review/skills/react/SKILL.md +0 -276
- package/plugins/devflow-code-review/skills/react/references/patterns.md +0 -1331
- package/plugins/devflow-core-skills/skills/accessibility/SKILL.md +0 -229
- package/plugins/devflow-core-skills/skills/accessibility/references/detection.md +0 -171
- package/plugins/devflow-core-skills/skills/accessibility/references/patterns.md +0 -670
- package/plugins/devflow-core-skills/skills/accessibility/references/violations.md +0 -419
- package/plugins/devflow-core-skills/skills/frontend-design/SKILL.md +0 -254
- package/plugins/devflow-core-skills/skills/frontend-design/references/detection.md +0 -184
- package/plugins/devflow-core-skills/skills/frontend-design/references/patterns.md +0 -511
- package/plugins/devflow-core-skills/skills/frontend-design/references/violations.md +0 -453
- package/plugins/devflow-core-skills/skills/react/references/violations.md +0 -565
- package/plugins/devflow-implement/skills/accessibility/SKILL.md +0 -229
- package/plugins/devflow-implement/skills/accessibility/references/detection.md +0 -171
- package/plugins/devflow-implement/skills/accessibility/references/patterns.md +0 -670
- package/plugins/devflow-implement/skills/accessibility/references/violations.md +0 -419
- package/plugins/devflow-implement/skills/frontend-design/SKILL.md +0 -254
- package/plugins/devflow-implement/skills/frontend-design/references/detection.md +0 -184
- package/plugins/devflow-implement/skills/frontend-design/references/patterns.md +0 -511
- package/plugins/devflow-implement/skills/frontend-design/references/violations.md +0 -453
- /package/plugins/{devflow-code-review → devflow-accessibility}/skills/accessibility/SKILL.md +0 -0
- /package/plugins/{devflow-code-review → devflow-accessibility}/skills/accessibility/references/detection.md +0 -0
- /package/plugins/{devflow-code-review → devflow-accessibility}/skills/accessibility/references/patterns.md +0 -0
- /package/plugins/{devflow-code-review → devflow-accessibility}/skills/accessibility/references/violations.md +0 -0
- /package/plugins/{devflow-code-review → devflow-frontend-design}/skills/frontend-design/SKILL.md +0 -0
- /package/plugins/{devflow-code-review → devflow-frontend-design}/skills/frontend-design/references/detection.md +0 -0
- /package/plugins/{devflow-code-review → devflow-frontend-design}/skills/frontend-design/references/patterns.md +0 -0
- /package/plugins/{devflow-code-review → devflow-frontend-design}/skills/frontend-design/references/violations.md +0 -0
- /package/plugins/{devflow-code-review → devflow-react}/skills/react/references/violations.md +0 -0
- /package/plugins/{devflow-core-skills → devflow-typescript}/skills/typescript/SKILL.md +0 -0
- /package/plugins/{devflow-core-skills → devflow-typescript}/skills/typescript/references/violations.md +0 -0
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
# TDD Rationalization Prevention — Extended Examples
|
|
2
|
+
|
|
3
|
+
Detailed code examples showing how each rationalization leads to worse outcomes.
|
|
4
|
+
|
|
5
|
+
## "I'll write tests after"
|
|
6
|
+
|
|
7
|
+
### What happens:
|
|
8
|
+
|
|
9
|
+
```typescript
|
|
10
|
+
// Developer writes production code first
|
|
11
|
+
function calculateDiscount(price: number, tier: string): number {
|
|
12
|
+
if (tier === 'gold') return price * 0.8;
|
|
13
|
+
if (tier === 'silver') return price * 0.9;
|
|
14
|
+
return price;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// Then "writes tests after" — but only for the happy path they remember
|
|
18
|
+
test('gold tier gets 20% off', () => {
|
|
19
|
+
expect(calculateDiscount(100, 'gold')).toBe(80);
|
|
20
|
+
});
|
|
21
|
+
// Missing: negative prices, unknown tiers, zero prices, NaN handling
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
### What TDD would have caught:
|
|
25
|
+
|
|
26
|
+
```typescript
|
|
27
|
+
// Test first — forces you to think about the contract
|
|
28
|
+
test('returns error for negative price', () => {
|
|
29
|
+
expect(calculateDiscount(-100, 'gold')).toEqual({ ok: false, error: 'NEGATIVE_PRICE' });
|
|
30
|
+
});
|
|
31
|
+
// Now the interface includes error handling from the start
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## "Too simple to test"
|
|
35
|
+
|
|
36
|
+
### What happens:
|
|
37
|
+
|
|
38
|
+
```typescript
|
|
39
|
+
// "It's just a config getter, no test needed"
|
|
40
|
+
function getMaxRetries(): number {
|
|
41
|
+
return parseInt(process.env.MAX_RETRIES || '3');
|
|
42
|
+
}
|
|
43
|
+
// 6 months later: someone sets MAX_RETRIES="three" and prod crashes with NaN retries
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
### What TDD would have caught:
|
|
47
|
+
|
|
48
|
+
```typescript
|
|
49
|
+
test('returns default when env var is not a number', () => {
|
|
50
|
+
process.env.MAX_RETRIES = 'three';
|
|
51
|
+
expect(getMaxRetries()).toBe(3); // Forces validation logic
|
|
52
|
+
});
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## "Test is too hard to write"
|
|
56
|
+
|
|
57
|
+
### What happens:
|
|
58
|
+
|
|
59
|
+
```typescript
|
|
60
|
+
// "I can't test this easily because it needs database + email + filesystem"
|
|
61
|
+
async function processOrder(orderId: string) {
|
|
62
|
+
const db = new Database();
|
|
63
|
+
const order = await db.find(orderId);
|
|
64
|
+
await sendEmail(order.customerEmail, 'Your order is processing');
|
|
65
|
+
await fs.writeFile(`/invoices/${orderId}.pdf`, generateInvoice(order));
|
|
66
|
+
await db.update(orderId, { status: 'processing' });
|
|
67
|
+
}
|
|
68
|
+
// Result: untestable monolith, test would need real DB + email + filesystem
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
### What TDD forces:
|
|
72
|
+
|
|
73
|
+
```typescript
|
|
74
|
+
// Hard-to-test = bad design. TDD forces dependency injection:
|
|
75
|
+
async function processOrder(
|
|
76
|
+
orderId: string,
|
|
77
|
+
deps: { db: OrderRepository; emailer: Emailer; invoices: InvoiceStore }
|
|
78
|
+
): Promise<Result<void, OrderError>> {
|
|
79
|
+
// Now trivially testable with mocks
|
|
80
|
+
}
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
## "I'll refactor later"
|
|
84
|
+
|
|
85
|
+
### What happens:
|
|
86
|
+
|
|
87
|
+
```typescript
|
|
88
|
+
// Sprint 1: "just get it working"
|
|
89
|
+
function handleRequest(req: any) {
|
|
90
|
+
if (req.type === 'create') { /* 50 lines */ }
|
|
91
|
+
else if (req.type === 'update') { /* 50 lines */ }
|
|
92
|
+
else if (req.type === 'delete') { /* 30 lines */ }
|
|
93
|
+
// Sprint 2-10: more conditions added, function grows to 500 lines
|
|
94
|
+
// "Refactor later" never comes because nobody wants to touch it
|
|
95
|
+
}
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
### What TDD enforces:
|
|
99
|
+
|
|
100
|
+
Step 3 (REFACTOR) happens every cycle. The function never grows beyond what's clean because you clean it every 5-10 minutes.
|
|
101
|
+
|
|
102
|
+
## "Tests slow me down"
|
|
103
|
+
|
|
104
|
+
### The math:
|
|
105
|
+
|
|
106
|
+
| Approach | Time to write | Time to first bug | Time to fix bug | Total (1 month) |
|
|
107
|
+
|----------|:---:|:---:|:---:|:---:|
|
|
108
|
+
| No TDD | 2h | 4h | 3h (no repro test) | 9h+ |
|
|
109
|
+
| TDD | 3h | Caught in test | 15min (test pinpoints) | 3h 15min |
|
|
110
|
+
|
|
111
|
+
TDD is slower for the first 30 minutes. It's faster for everything after that.
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "devflow-frontend-design",
|
|
3
|
+
"description": "Frontend design patterns - typography, color systems, spacing, motion, responsive design",
|
|
4
|
+
"author": {
|
|
5
|
+
"name": "DevFlow Contributors",
|
|
6
|
+
"email": "dean@keren.dev"
|
|
7
|
+
},
|
|
8
|
+
"version": "1.2.0",
|
|
9
|
+
"homepage": "https://github.com/dean0x/devflow",
|
|
10
|
+
"repository": "https://github.com/dean0x/devflow",
|
|
11
|
+
"license": "MIT",
|
|
12
|
+
"keywords": ["design", "typography", "color", "css"],
|
|
13
|
+
"agents": [],
|
|
14
|
+
"skills": ["frontend-design"]
|
|
15
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "devflow-go",
|
|
3
|
+
"description": "Go language patterns - error handling, interfaces, concurrency, package design",
|
|
4
|
+
"author": {
|
|
5
|
+
"name": "DevFlow Contributors",
|
|
6
|
+
"email": "dean@keren.dev"
|
|
7
|
+
},
|
|
8
|
+
"version": "1.2.0",
|
|
9
|
+
"homepage": "https://github.com/dean0x/devflow",
|
|
10
|
+
"repository": "https://github.com/dean0x/devflow",
|
|
11
|
+
"license": "MIT",
|
|
12
|
+
"keywords": ["go", "golang", "concurrency", "errors"],
|
|
13
|
+
"agents": [],
|
|
14
|
+
"skills": ["go"]
|
|
15
|
+
}
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: go
|
|
3
|
+
description: This skill should be used when the user works with Go files (.go), asks about "error handling", "interfaces", "goroutines", "channels", "packages", or discusses Go idioms and concurrency. Provides patterns for error handling, interface design, concurrency, and package organization.
|
|
4
|
+
user-invocable: false
|
|
5
|
+
allowed-tools: Read, Grep, Glob
|
|
6
|
+
activation:
|
|
7
|
+
file-patterns:
|
|
8
|
+
- "**/*.go"
|
|
9
|
+
exclude:
|
|
10
|
+
- "vendor/**"
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
# Go Patterns
|
|
14
|
+
|
|
15
|
+
Reference for Go-specific patterns, idioms, and best practices.
|
|
16
|
+
|
|
17
|
+
## Iron Law
|
|
18
|
+
|
|
19
|
+
> **ERRORS ARE VALUES**
|
|
20
|
+
>
|
|
21
|
+
> Never ignore errors. `if err != nil` is correctness, not boilerplate. Every error
|
|
22
|
+
> return must be checked, wrapped with context, or explicitly documented as intentionally
|
|
23
|
+
> ignored with `_ = fn()`. Silent error swallowing causes cascading failures.
|
|
24
|
+
|
|
25
|
+
## When This Skill Activates
|
|
26
|
+
|
|
27
|
+
- Working with Go codebases
|
|
28
|
+
- Designing interfaces and packages
|
|
29
|
+
- Implementing concurrent code
|
|
30
|
+
- Handling errors
|
|
31
|
+
- Structuring Go projects
|
|
32
|
+
|
|
33
|
+
---
|
|
34
|
+
|
|
35
|
+
## Error Handling
|
|
36
|
+
|
|
37
|
+
### Wrap Errors with Context
|
|
38
|
+
|
|
39
|
+
```go
|
|
40
|
+
// BAD: return err
|
|
41
|
+
// GOOD: return fmt.Errorf("reading config %s: %w", path, err)
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
### Sentinel Errors for Expected Conditions
|
|
45
|
+
|
|
46
|
+
```go
|
|
47
|
+
var ErrNotFound = errors.New("not found")
|
|
48
|
+
|
|
49
|
+
func FindUser(id string) (*User, error) {
|
|
50
|
+
u, err := db.Get(id)
|
|
51
|
+
if err != nil {
|
|
52
|
+
return nil, fmt.Errorf("finding user %s: %w", id, err)
|
|
53
|
+
}
|
|
54
|
+
if u == nil {
|
|
55
|
+
return nil, ErrNotFound
|
|
56
|
+
}
|
|
57
|
+
return u, nil
|
|
58
|
+
}
|
|
59
|
+
// Caller: if errors.Is(err, ErrNotFound) { ... }
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
---
|
|
63
|
+
|
|
64
|
+
## Interface Design
|
|
65
|
+
|
|
66
|
+
### Accept Interfaces, Return Structs
|
|
67
|
+
|
|
68
|
+
```go
|
|
69
|
+
// BAD: func NewService(repo *PostgresRepo) *Service
|
|
70
|
+
// GOOD: func NewService(repo Repository) *Service
|
|
71
|
+
|
|
72
|
+
type Repository interface {
|
|
73
|
+
FindByID(ctx context.Context, id string) (*Entity, error)
|
|
74
|
+
Save(ctx context.Context, entity *Entity) error
|
|
75
|
+
}
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
### Keep Interfaces Small
|
|
79
|
+
|
|
80
|
+
```go
|
|
81
|
+
// BAD: 10-method interface
|
|
82
|
+
// GOOD: single-method interfaces composed as needed
|
|
83
|
+
type Reader interface { Read(p []byte) (n int, err error) }
|
|
84
|
+
type Writer interface { Write(p []byte) (n int, err error) }
|
|
85
|
+
type ReadWriter interface { Reader; Writer }
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
---
|
|
89
|
+
|
|
90
|
+
## Concurrency
|
|
91
|
+
|
|
92
|
+
### Use Context for Cancellation
|
|
93
|
+
|
|
94
|
+
```go
|
|
95
|
+
func Process(ctx context.Context, items []Item) error {
|
|
96
|
+
g, ctx := errgroup.WithContext(ctx)
|
|
97
|
+
for _, item := range items {
|
|
98
|
+
g.Go(func() error {
|
|
99
|
+
return processItem(ctx, item)
|
|
100
|
+
})
|
|
101
|
+
}
|
|
102
|
+
return g.Wait()
|
|
103
|
+
}
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
### Channel Direction
|
|
107
|
+
|
|
108
|
+
```go
|
|
109
|
+
// Declare direction in function signatures
|
|
110
|
+
func producer(ch chan<- int) { ch <- 42 }
|
|
111
|
+
func consumer(ch <-chan int) { v := <-ch; _ = v }
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
---
|
|
115
|
+
|
|
116
|
+
## Package Design
|
|
117
|
+
|
|
118
|
+
### Organize by Domain, Not by Type
|
|
119
|
+
|
|
120
|
+
```go
|
|
121
|
+
// BAD: models/, controllers/, services/
|
|
122
|
+
// GOOD: user/, order/, payment/
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
### Export Only What's Needed
|
|
126
|
+
|
|
127
|
+
```go
|
|
128
|
+
// Internal helpers stay unexported (lowercase)
|
|
129
|
+
func (s *Service) validate(u *User) error { ... }
|
|
130
|
+
|
|
131
|
+
// Public API is exported (uppercase)
|
|
132
|
+
func (s *Service) CreateUser(ctx context.Context, req CreateUserReq) (*User, error) { ... }
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
---
|
|
136
|
+
|
|
137
|
+
## Zero Values
|
|
138
|
+
|
|
139
|
+
```go
|
|
140
|
+
// Use zero values as valid defaults
|
|
141
|
+
var mu sync.Mutex // Ready to use
|
|
142
|
+
var buf bytes.Buffer // Ready to use
|
|
143
|
+
var wg sync.WaitGroup // Ready to use
|
|
144
|
+
|
|
145
|
+
// Design types with useful zero values
|
|
146
|
+
type Config struct {
|
|
147
|
+
Timeout time.Duration // zero = no timeout
|
|
148
|
+
Retries int // zero = no retries
|
|
149
|
+
}
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
---
|
|
153
|
+
|
|
154
|
+
## Anti-Patterns
|
|
155
|
+
|
|
156
|
+
| Pattern | Bad | Good |
|
|
157
|
+
|---------|-----|------|
|
|
158
|
+
| Ignoring error | `val, _ := fn()` | `val, err := fn(); if err != nil { ... }` |
|
|
159
|
+
| Naked return | `return` in named returns | Explicit `return val, err` |
|
|
160
|
+
| init() abuse | Complex `init()` functions | Explicit initialization in `main()` or constructors |
|
|
161
|
+
| Interface pollution | Defining interfaces before use | Define interfaces at the consumer site |
|
|
162
|
+
| Goroutine leak | `go fn()` without lifecycle | Use context, errgroup, or done channels |
|
|
163
|
+
|
|
164
|
+
---
|
|
165
|
+
|
|
166
|
+
## Extended References
|
|
167
|
+
|
|
168
|
+
For additional patterns and examples:
|
|
169
|
+
- `references/violations.md` - Common Go violations
|
|
170
|
+
- `references/patterns.md` - Extended Go patterns
|
|
171
|
+
- `references/detection.md` - Detection patterns for Go issues
|
|
172
|
+
- `references/concurrency.md` - Advanced concurrency patterns
|
|
173
|
+
|
|
174
|
+
---
|
|
175
|
+
|
|
176
|
+
## Checklist
|
|
177
|
+
|
|
178
|
+
- [ ] All errors checked or explicitly ignored with `_ =`
|
|
179
|
+
- [ ] Errors wrapped with `fmt.Errorf("context: %w", err)`
|
|
180
|
+
- [ ] Interfaces defined at consumer, not producer
|
|
181
|
+
- [ ] Interfaces kept small (1-3 methods)
|
|
182
|
+
- [ ] Context passed as first parameter
|
|
183
|
+
- [ ] Goroutines have clear lifecycle/cancellation
|
|
184
|
+
- [ ] Channel direction specified in signatures
|
|
185
|
+
- [ ] Zero values are useful defaults
|
|
186
|
+
- [ ] Packages organized by domain
|
|
187
|
+
- [ ] No `init()` with side effects
|
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
# Go Concurrency Patterns
|
|
2
|
+
|
|
3
|
+
Advanced concurrency patterns for Go. Reference from main SKILL.md.
|
|
4
|
+
|
|
5
|
+
## errgroup for Structured Concurrency
|
|
6
|
+
|
|
7
|
+
```go
|
|
8
|
+
import "golang.org/x/sync/errgroup"
|
|
9
|
+
|
|
10
|
+
func FetchAll(ctx context.Context, urls []string) ([]Response, error) {
|
|
11
|
+
g, ctx := errgroup.WithContext(ctx)
|
|
12
|
+
responses := make([]Response, len(urls))
|
|
13
|
+
|
|
14
|
+
for i, url := range urls {
|
|
15
|
+
g.Go(func() error {
|
|
16
|
+
resp, err := fetch(ctx, url)
|
|
17
|
+
if err != nil {
|
|
18
|
+
return fmt.Errorf("fetching %s: %w", url, err)
|
|
19
|
+
}
|
|
20
|
+
responses[i] = resp // Safe: each goroutine writes to unique index
|
|
21
|
+
return nil
|
|
22
|
+
})
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
if err := g.Wait(); err != nil {
|
|
26
|
+
return nil, err
|
|
27
|
+
}
|
|
28
|
+
return responses, nil
|
|
29
|
+
}
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
### errgroup with Concurrency Limit
|
|
33
|
+
|
|
34
|
+
```go
|
|
35
|
+
func ProcessItems(ctx context.Context, items []Item) error {
|
|
36
|
+
g, ctx := errgroup.WithContext(ctx)
|
|
37
|
+
g.SetLimit(10) // Maximum 10 concurrent goroutines
|
|
38
|
+
|
|
39
|
+
for _, item := range items {
|
|
40
|
+
g.Go(func() error {
|
|
41
|
+
return processItem(ctx, item)
|
|
42
|
+
})
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return g.Wait()
|
|
46
|
+
}
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
---
|
|
50
|
+
|
|
51
|
+
## Worker Pool
|
|
52
|
+
|
|
53
|
+
```go
|
|
54
|
+
func WorkerPool(ctx context.Context, jobs <-chan Job, workers int) <-chan Result {
|
|
55
|
+
results := make(chan Result, workers)
|
|
56
|
+
|
|
57
|
+
var wg sync.WaitGroup
|
|
58
|
+
for range workers {
|
|
59
|
+
wg.Add(1)
|
|
60
|
+
go func() {
|
|
61
|
+
defer wg.Done()
|
|
62
|
+
for {
|
|
63
|
+
select {
|
|
64
|
+
case job, ok := <-jobs:
|
|
65
|
+
if !ok {
|
|
66
|
+
return
|
|
67
|
+
}
|
|
68
|
+
results <- process(ctx, job)
|
|
69
|
+
case <-ctx.Done():
|
|
70
|
+
return
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}()
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
go func() {
|
|
77
|
+
wg.Wait()
|
|
78
|
+
close(results)
|
|
79
|
+
}()
|
|
80
|
+
|
|
81
|
+
return results
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Usage
|
|
85
|
+
jobs := make(chan Job, 100)
|
|
86
|
+
results := WorkerPool(ctx, jobs, 5)
|
|
87
|
+
|
|
88
|
+
// Send jobs
|
|
89
|
+
go func() {
|
|
90
|
+
defer close(jobs)
|
|
91
|
+
for _, j := range allJobs {
|
|
92
|
+
jobs <- j
|
|
93
|
+
}
|
|
94
|
+
}()
|
|
95
|
+
|
|
96
|
+
// Collect results
|
|
97
|
+
for r := range results {
|
|
98
|
+
fmt.Println(r)
|
|
99
|
+
}
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
---
|
|
103
|
+
|
|
104
|
+
## Fan-Out / Fan-In
|
|
105
|
+
|
|
106
|
+
```go
|
|
107
|
+
// Fan-out: one source, multiple workers
|
|
108
|
+
func fanOut(ctx context.Context, input <-chan int, workers int) []<-chan int {
|
|
109
|
+
channels := make([]<-chan int, workers)
|
|
110
|
+
for i := range workers {
|
|
111
|
+
channels[i] = worker(ctx, input)
|
|
112
|
+
}
|
|
113
|
+
return channels
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
func worker(ctx context.Context, input <-chan int) <-chan int {
|
|
117
|
+
out := make(chan int)
|
|
118
|
+
go func() {
|
|
119
|
+
defer close(out)
|
|
120
|
+
for n := range input {
|
|
121
|
+
select {
|
|
122
|
+
case out <- n * n:
|
|
123
|
+
case <-ctx.Done():
|
|
124
|
+
return
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}()
|
|
128
|
+
return out
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Fan-in: multiple sources, one destination
|
|
132
|
+
func fanIn(ctx context.Context, channels ...<-chan int) <-chan int {
|
|
133
|
+
merged := make(chan int)
|
|
134
|
+
var wg sync.WaitGroup
|
|
135
|
+
|
|
136
|
+
for _, ch := range channels {
|
|
137
|
+
wg.Add(1)
|
|
138
|
+
go func() {
|
|
139
|
+
defer wg.Done()
|
|
140
|
+
for val := range ch {
|
|
141
|
+
select {
|
|
142
|
+
case merged <- val:
|
|
143
|
+
case <-ctx.Done():
|
|
144
|
+
return
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}()
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
go func() {
|
|
151
|
+
wg.Wait()
|
|
152
|
+
close(merged)
|
|
153
|
+
}()
|
|
154
|
+
|
|
155
|
+
return merged
|
|
156
|
+
}
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
---
|
|
160
|
+
|
|
161
|
+
## Select with Timeout
|
|
162
|
+
|
|
163
|
+
```go
|
|
164
|
+
func fetchWithTimeout(ctx context.Context, url string) ([]byte, error) {
|
|
165
|
+
ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
|
|
166
|
+
defer cancel()
|
|
167
|
+
|
|
168
|
+
ch := make(chan result, 1)
|
|
169
|
+
go func() {
|
|
170
|
+
data, err := doFetch(ctx, url)
|
|
171
|
+
ch <- result{data, err}
|
|
172
|
+
}()
|
|
173
|
+
|
|
174
|
+
select {
|
|
175
|
+
case r := <-ch:
|
|
176
|
+
return r.data, r.err
|
|
177
|
+
case <-ctx.Done():
|
|
178
|
+
return nil, fmt.Errorf("fetch %s: %w", url, ctx.Err())
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
type result struct {
|
|
183
|
+
data []byte
|
|
184
|
+
err error
|
|
185
|
+
}
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
### Select for Multiple Sources
|
|
189
|
+
|
|
190
|
+
```go
|
|
191
|
+
func merge(ctx context.Context, primary, fallback <-chan Event) <-chan Event {
|
|
192
|
+
out := make(chan Event)
|
|
193
|
+
go func() {
|
|
194
|
+
defer close(out)
|
|
195
|
+
for {
|
|
196
|
+
select {
|
|
197
|
+
case e, ok := <-primary:
|
|
198
|
+
if !ok {
|
|
199
|
+
return
|
|
200
|
+
}
|
|
201
|
+
out <- e
|
|
202
|
+
case e, ok := <-fallback:
|
|
203
|
+
if !ok {
|
|
204
|
+
return
|
|
205
|
+
}
|
|
206
|
+
out <- e
|
|
207
|
+
case <-ctx.Done():
|
|
208
|
+
return
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}()
|
|
212
|
+
return out
|
|
213
|
+
}
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
---
|
|
217
|
+
|
|
218
|
+
## Mutex vs Channels
|
|
219
|
+
|
|
220
|
+
### Use Mutex When
|
|
221
|
+
|
|
222
|
+
```go
|
|
223
|
+
// Protecting shared state with simple read/write
|
|
224
|
+
type SafeCounter struct {
|
|
225
|
+
mu sync.RWMutex
|
|
226
|
+
v map[string]int
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
func (c *SafeCounter) Inc(key string) {
|
|
230
|
+
c.mu.Lock()
|
|
231
|
+
defer c.mu.Unlock()
|
|
232
|
+
c.v[key]++
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
func (c *SafeCounter) Get(key string) int {
|
|
236
|
+
c.mu.RLock()
|
|
237
|
+
defer c.mu.RUnlock()
|
|
238
|
+
return c.v[key]
|
|
239
|
+
}
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
### Use Channels When
|
|
243
|
+
|
|
244
|
+
```go
|
|
245
|
+
// Communicating between goroutines / coordinating work
|
|
246
|
+
func pipeline(ctx context.Context, input []int) <-chan int {
|
|
247
|
+
out := make(chan int)
|
|
248
|
+
go func() {
|
|
249
|
+
defer close(out)
|
|
250
|
+
for _, n := range input {
|
|
251
|
+
select {
|
|
252
|
+
case out <- transform(n):
|
|
253
|
+
case <-ctx.Done():
|
|
254
|
+
return
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
}()
|
|
258
|
+
return out
|
|
259
|
+
}
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
### Decision Guide
|
|
263
|
+
|
|
264
|
+
| Scenario | Use |
|
|
265
|
+
|----------|-----|
|
|
266
|
+
| Guarding shared state | `sync.Mutex` or `sync.RWMutex` |
|
|
267
|
+
| Passing ownership of data | Channels |
|
|
268
|
+
| Coordinating goroutine lifecycle | `context.Context` + channels |
|
|
269
|
+
| Waiting for N goroutines | `sync.WaitGroup` or `errgroup` |
|
|
270
|
+
| One-time initialization | `sync.Once` |
|
|
271
|
+
| Concurrent map access | `sync.Map` (high read, low write) |
|
|
272
|
+
|
|
273
|
+
---
|
|
274
|
+
|
|
275
|
+
## sync.Once for Initialization
|
|
276
|
+
|
|
277
|
+
```go
|
|
278
|
+
type Client struct {
|
|
279
|
+
once sync.Once
|
|
280
|
+
conn *grpc.ClientConn
|
|
281
|
+
err error
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
func (c *Client) connection() (*grpc.ClientConn, error) {
|
|
285
|
+
c.once.Do(func() {
|
|
286
|
+
// requires: "google.golang.org/grpc/credentials/insecure"
|
|
287
|
+
c.conn, c.err = grpc.Dial("localhost:50051",
|
|
288
|
+
grpc.WithTransportCredentials(insecure.NewCredentials()))
|
|
289
|
+
})
|
|
290
|
+
return c.conn, c.err
|
|
291
|
+
}
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
---
|
|
295
|
+
|
|
296
|
+
## Rate Limiting
|
|
297
|
+
|
|
298
|
+
```go
|
|
299
|
+
func rateLimited(ctx context.Context, items []Item, rps int) error {
|
|
300
|
+
limiter := rate.NewLimiter(rate.Limit(rps), 1)
|
|
301
|
+
|
|
302
|
+
for _, item := range items {
|
|
303
|
+
if err := limiter.Wait(ctx); err != nil {
|
|
304
|
+
return fmt.Errorf("rate limiter: %w", err)
|
|
305
|
+
}
|
|
306
|
+
if err := process(ctx, item); err != nil {
|
|
307
|
+
return fmt.Errorf("processing item %s: %w", item.ID, err)
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
return nil
|
|
311
|
+
}
|
|
312
|
+
```
|