@coralai/sps-cli 0.41.2 → 0.43.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/README.md +34 -3
- package/dist/commands/cardAdd.d.ts +1 -1
- package/dist/commands/cardAdd.d.ts.map +1 -1
- package/dist/commands/cardAdd.js +16 -6
- package/dist/commands/cardAdd.js.map +1 -1
- package/dist/commands/cardDashboard.js +1 -1
- package/dist/commands/cardDashboard.js.map +1 -1
- package/dist/commands/doctor.d.ts +9 -0
- package/dist/commands/doctor.d.ts.map +1 -1
- package/dist/commands/doctor.js +3 -314
- package/dist/commands/doctor.js.map +1 -1
- package/dist/commands/hookCommand.d.ts.map +1 -1
- package/dist/commands/hookCommand.js +6 -7
- package/dist/commands/hookCommand.js.map +1 -1
- package/dist/commands/pmCommand.js +1 -1
- package/dist/commands/pmCommand.js.map +1 -1
- package/dist/commands/projectInit.d.ts.map +1 -1
- package/dist/commands/projectInit.js +60 -37
- package/dist/commands/projectInit.js.map +1 -1
- package/dist/commands/setup.d.ts.map +1 -1
- package/dist/commands/setup.js +3 -30
- package/dist/commands/setup.js.map +1 -1
- package/dist/commands/skillCommand.d.ts +2 -0
- package/dist/commands/skillCommand.d.ts.map +1 -0
- package/dist/commands/skillCommand.js +235 -0
- package/dist/commands/skillCommand.js.map +1 -0
- package/dist/commands/tick.js +1 -1
- package/dist/commands/tick.js.map +1 -1
- package/dist/core/checklist.d.ts +22 -0
- package/dist/core/checklist.d.ts.map +1 -0
- package/dist/core/checklist.js +38 -0
- package/dist/core/checklist.js.map +1 -0
- package/dist/core/checklist.test.d.ts +2 -0
- package/dist/core/checklist.test.d.ts.map +1 -0
- package/dist/core/checklist.test.js +74 -0
- package/dist/core/checklist.test.js.map +1 -0
- package/dist/core/config.d.ts +1 -1
- package/dist/core/config.d.ts.map +1 -1
- package/dist/core/config.js +1 -1
- package/dist/core/config.js.map +1 -1
- package/dist/core/config.test.js +7 -4
- package/dist/core/config.test.js.map +1 -1
- package/dist/core/context.d.ts +1 -1
- package/dist/core/context.d.ts.map +1 -1
- package/dist/core/skillStore.d.ts +46 -0
- package/dist/core/skillStore.d.ts.map +1 -0
- package/dist/core/skillStore.js +197 -0
- package/dist/core/skillStore.js.map +1 -0
- package/dist/core/skillStore.test.d.ts +2 -0
- package/dist/core/skillStore.test.d.ts.map +1 -0
- package/dist/core/skillStore.test.js +190 -0
- package/dist/core/skillStore.test.js.map +1 -0
- package/dist/engines/EventHandler.test.js +3 -3
- package/dist/engines/EventHandler.test.js.map +1 -1
- package/dist/engines/MonitorEngine.js +2 -2
- package/dist/engines/MonitorEngine.js.map +1 -1
- package/dist/engines/SchedulerEngine.js +1 -1
- package/dist/engines/SchedulerEngine.js.map +1 -1
- package/dist/engines/StageEngine.js +3 -3
- package/dist/engines/StageEngine.js.map +1 -1
- package/dist/engines/engine-pipeline-adapter.test.js +2 -2
- package/dist/engines/engine-pipeline-adapter.test.js.map +1 -1
- package/dist/interfaces/TaskBackend.d.ts +3 -1
- package/dist/interfaces/TaskBackend.d.ts.map +1 -1
- package/dist/main.js +19 -17
- package/dist/main.js.map +1 -1
- package/dist/models/types.d.ts +16 -1
- package/dist/models/types.d.ts.map +1 -1
- package/dist/providers/MarkdownTaskBackend.d.ts +2 -1
- package/dist/providers/MarkdownTaskBackend.d.ts.map +1 -1
- package/dist/providers/MarkdownTaskBackend.js +28 -5
- package/dist/providers/MarkdownTaskBackend.js.map +1 -1
- package/dist/providers/registry.d.ts.map +1 -1
- package/dist/providers/registry.js +5 -7
- package/dist/providers/registry.js.map +1 -1
- package/package.json +1 -1
- package/project-template/.claude/hooks/start.sh +44 -0
- package/project-template/.claude/settings.json +1 -1
- package/skills/architecture-decision-records/SKILL.md +207 -0
- package/skills/backend/SKILL.md +62 -0
- package/skills/backend/references/api-design.md +168 -0
- package/skills/backend/references/caching.md +181 -0
- package/skills/backend/references/data-access.md +173 -0
- package/skills/backend/references/layering.md +181 -0
- package/skills/backend/references/observability.md +190 -0
- package/skills/backend/references/resilience.md +201 -0
- package/skills/backend/references/security.md +186 -0
- package/skills/backend-architect/SKILL.md +119 -0
- package/skills/code-reviewer/SKILL.md +143 -0
- package/skills/coding-standards/SKILL.md +60 -0
- package/skills/coding-standards/references/clean-code.md +258 -0
- package/skills/coding-standards/references/code-review.md +192 -0
- package/skills/coding-standards/references/commits-and-prs.md +226 -0
- package/skills/coding-standards/references/error-strategy.md +193 -0
- package/skills/coding-standards/references/naming.md +185 -0
- package/skills/coding-standards/references/tdd.md +171 -0
- package/skills/database/SKILL.md +53 -0
- package/skills/database/references/indexing.md +190 -0
- package/skills/database/references/migrations.md +199 -0
- package/skills/database/references/nosql.md +185 -0
- package/skills/database/references/queries.md +295 -0
- package/skills/database/references/scaling.md +203 -0
- package/skills/database/references/schema.md +191 -0
- package/skills/database-optimizer/SKILL.md +168 -0
- package/skills/debugging-workflow/SKILL.md +244 -0
- package/skills/devops/SKILL.md +55 -0
- package/skills/devops/references/ci-cd.md +204 -0
- package/skills/devops/references/containers.md +272 -0
- package/skills/devops/references/deploy.md +201 -0
- package/skills/devops/references/iac.md +252 -0
- package/skills/devops/references/observability.md +228 -0
- package/skills/devops/references/secrets.md +178 -0
- package/skills/devops-automator/SKILL.md +164 -0
- package/skills/frontend/SKILL.md +52 -0
- package/skills/frontend/references/accessibility.md +222 -0
- package/skills/frontend/references/components.md +206 -0
- package/skills/frontend/references/performance.md +219 -0
- package/skills/frontend/references/routing.md +209 -0
- package/skills/frontend/references/state.md +190 -0
- package/skills/frontend/references/testing.md +216 -0
- package/skills/frontend-developer/SKILL.md +115 -0
- package/skills/git-workflow/SKILL.md +355 -0
- package/skills/golang/SKILL.md +49 -0
- package/skills/golang/references/concurrency.md +284 -0
- package/skills/golang/references/errors.md +241 -0
- package/skills/golang/references/idioms.md +285 -0
- package/skills/golang/references/testing.md +238 -0
- package/skills/java/SKILL.md +50 -0
- package/skills/java/references/concurrency.md +194 -0
- package/skills/java/references/idioms.md +283 -0
- package/skills/java/references/testing.md +228 -0
- package/skills/kotlin/SKILL.md +47 -0
- package/skills/kotlin/references/coroutines.md +240 -0
- package/skills/kotlin/references/idioms.md +268 -0
- package/skills/kotlin/references/testing.md +219 -0
- package/skills/mobile/SKILL.md +50 -0
- package/skills/mobile/references/architecture.md +204 -0
- package/skills/mobile/references/navigation.md +158 -0
- package/skills/mobile/references/performance.md +152 -0
- package/skills/mobile/references/platform.md +166 -0
- package/skills/mobile/references/state-and-data.md +174 -0
- package/skills/python/SKILL.md +51 -0
- package/skills/python/THIRD_PARTY.md +14 -0
- package/skills/python/references/async.md +218 -0
- package/skills/python/references/error-handling.md +254 -0
- package/skills/python/references/idioms.md +279 -0
- package/skills/python/references/packaging.md +233 -0
- package/skills/python/references/testing.md +269 -0
- package/skills/python/references/typing.md +292 -0
- package/skills/qa-tester/SKILL.md +186 -0
- package/skills/rust/SKILL.md +50 -0
- package/skills/rust/references/async.md +224 -0
- package/skills/rust/references/errors.md +240 -0
- package/skills/rust/references/ownership.md +263 -0
- package/skills/rust/references/testing.md +274 -0
- package/skills/rust/references/traits.md +250 -0
- package/skills/security-engineer/SKILL.md +157 -0
- package/skills/swift/SKILL.md +48 -0
- package/skills/swift/references/concurrency.md +280 -0
- package/skills/swift/references/idioms.md +334 -0
- package/skills/swift/references/testing.md +229 -0
- package/skills/typescript/SKILL.md +51 -0
- package/skills/typescript/references/async.md +241 -0
- package/skills/typescript/references/errors.md +208 -0
- package/skills/typescript/references/idioms.md +246 -0
- package/skills/typescript/references/testing.md +225 -0
- package/skills/typescript/references/tooling.md +208 -0
- package/skills/typescript/references/types.md +259 -0
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: golang
|
|
3
|
+
description: Go language skill — idioms, errors, concurrency, testing. Pair with end skills (`backend`, `devops`) for architecture and with `coding-standards` for cross-language principles.
|
|
4
|
+
origin: ecc-fork + original (https://github.com/affaan-m/everything-claude-code, MIT)
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Go
|
|
8
|
+
|
|
9
|
+
Go is small on purpose. Few idioms, clear conventions, one canonical way to do most things. Fight the language less, follow its grain more.
|
|
10
|
+
|
|
11
|
+
## When to load
|
|
12
|
+
|
|
13
|
+
- Project primary language is Go
|
|
14
|
+
- Reviewing Go code
|
|
15
|
+
- Designing packages, interfaces, error types
|
|
16
|
+
- Concurrency with goroutines, channels, `context.Context`
|
|
17
|
+
|
|
18
|
+
## Core principles
|
|
19
|
+
|
|
20
|
+
1. **Clear over clever.** Short functions, short files, short package names.
|
|
21
|
+
2. **Return errors; never panic at API boundaries.** Panics are for unrecoverable bugs, not flow control.
|
|
22
|
+
3. **Accept interfaces, return structs.** The caller owns the abstraction.
|
|
23
|
+
4. **Interfaces live at the call site** (consumer-defined). Don't define an interface until you have two implementations or a test double.
|
|
24
|
+
5. **`context.Context` as the first parameter** on anything that does I/O or can be cancelled.
|
|
25
|
+
6. **Goroutines need a lifecycle.** Every `go f()` needs a story: how it stops, who waits, what happens on error.
|
|
26
|
+
7. **No hidden control flow.** No macros, no implicit constructors, no `this`. If it runs, it's in the code you see.
|
|
27
|
+
8. **Format with `gofmt`.** Style is not a discussion.
|
|
28
|
+
|
|
29
|
+
## How to use references
|
|
30
|
+
|
|
31
|
+
| Reference | When to load |
|
|
32
|
+
|---|---|
|
|
33
|
+
| [`references/idioms.md`](references/idioms.md) | Package layout, naming, zero values, struct embedding, slices vs. arrays |
|
|
34
|
+
| [`references/errors.md`](references/errors.md) | `error` interface, wrapping (`%w`), `errors.Is/As`, sentinel vs. typed errors |
|
|
35
|
+
| [`references/concurrency.md`](references/concurrency.md) | Goroutines, channels, `sync`, `context.Context`, cancellation, `errgroup` |
|
|
36
|
+
| [`references/testing.md`](references/testing.md) | `testing` package, table-driven tests, `t.Run`, `testify`, fuzzing, benchmarks |
|
|
37
|
+
|
|
38
|
+
## Forbidden patterns (auto-reject)
|
|
39
|
+
|
|
40
|
+
- `panic` outside of `init()` or truly unrecoverable state
|
|
41
|
+
- Ignoring errors with `_`, except in narrow documented cases
|
|
42
|
+
- Goroutines without a cancellation path / termination contract
|
|
43
|
+
- Blocking operations without `context.Context`
|
|
44
|
+
- Interface defined in the package that provides the implementation (should live at call site)
|
|
45
|
+
- `init()` doing I/O or mutating globals
|
|
46
|
+
- Returning named result parameters just to "save" a `return` statement
|
|
47
|
+
- Global mutable state for business data (singletons, package-level vars)
|
|
48
|
+
- Allocating in a hot loop when `sync.Pool` or preallocation would fix it
|
|
49
|
+
- Using `interface{}` / `any` when a concrete type is known
|
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
# Go — Concurrency
|
|
2
|
+
|
|
3
|
+
Goroutines, channels, `sync`, `context.Context`, `errgroup`.
|
|
4
|
+
|
|
5
|
+
## The rules
|
|
6
|
+
|
|
7
|
+
1. Every goroutine must have a way to terminate.
|
|
8
|
+
2. Every blocking operation takes a `context.Context`.
|
|
9
|
+
3. Channels are for communication, not for every synchronization need. `sync.Mutex` and `sync.WaitGroup` are first-class tools.
|
|
10
|
+
4. "Do not communicate by sharing memory; share memory by communicating" is a guideline, not a law. Use what fits.
|
|
11
|
+
|
|
12
|
+
## `context.Context`
|
|
13
|
+
|
|
14
|
+
First parameter, always.
|
|
15
|
+
|
|
16
|
+
```go
|
|
17
|
+
func (s *Service) Get(ctx context.Context, id string) (*User, error) {
|
|
18
|
+
// pass ctx to every I/O call
|
|
19
|
+
row := s.db.QueryRowContext(ctx, "select ... where id=$1", id)
|
|
20
|
+
...
|
|
21
|
+
}
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Responsibilities:
|
|
25
|
+
- **Cancellation** — `<-ctx.Done()` fires when the parent cancels.
|
|
26
|
+
- **Deadline** — `context.WithTimeout(ctx, 5*time.Second)`.
|
|
27
|
+
- **Request-scoped values** — small, process-wide identifiers (trace id, auth principal). NOT a general DI container.
|
|
28
|
+
|
|
29
|
+
Rules:
|
|
30
|
+
- Never store a context in a struct.
|
|
31
|
+
- Never pass `nil` context; use `context.TODO()` if you really don't have one yet (and fix it).
|
|
32
|
+
- Never ignore a cancelled context and continue — return the error.
|
|
33
|
+
|
|
34
|
+
## Goroutine lifecycle
|
|
35
|
+
|
|
36
|
+
```go
|
|
37
|
+
// ❌ detached; we have no way to stop or wait
|
|
38
|
+
go doWork()
|
|
39
|
+
|
|
40
|
+
// ✅ bounded by context; caller decides when we stop
|
|
41
|
+
go func() {
|
|
42
|
+
for {
|
|
43
|
+
select {
|
|
44
|
+
case <-ctx.Done():
|
|
45
|
+
return
|
|
46
|
+
case <-ticker.C:
|
|
47
|
+
doWork()
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}()
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Rule: if you spawn a goroutine, write down (in a comment or a test) how it exits.
|
|
54
|
+
|
|
55
|
+
## WaitGroups
|
|
56
|
+
|
|
57
|
+
For "wait for N goroutines to finish".
|
|
58
|
+
|
|
59
|
+
```go
|
|
60
|
+
var wg sync.WaitGroup
|
|
61
|
+
for _, x := range xs {
|
|
62
|
+
wg.Add(1)
|
|
63
|
+
go func(x T) {
|
|
64
|
+
defer wg.Done()
|
|
65
|
+
process(x)
|
|
66
|
+
}(x)
|
|
67
|
+
}
|
|
68
|
+
wg.Wait()
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
Don't call `wg.Add` from inside the goroutine (race). Add before `go`.
|
|
72
|
+
|
|
73
|
+
## `errgroup` — WaitGroup + errors + cancellation
|
|
74
|
+
|
|
75
|
+
`golang.org/x/sync/errgroup` is the right default for concurrent work that can fail.
|
|
76
|
+
|
|
77
|
+
```go
|
|
78
|
+
import "golang.org/x/sync/errgroup"
|
|
79
|
+
|
|
80
|
+
g, ctx := errgroup.WithContext(ctx)
|
|
81
|
+
for _, url := range urls {
|
|
82
|
+
url := url // shadow (pre-Go-1.22)
|
|
83
|
+
g.Go(func() error {
|
|
84
|
+
return fetch(ctx, url)
|
|
85
|
+
})
|
|
86
|
+
}
|
|
87
|
+
if err := g.Wait(); err != nil {
|
|
88
|
+
return err // first error; ctx is cancelled → others stop
|
|
89
|
+
}
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
Bounded concurrency:
|
|
93
|
+
```go
|
|
94
|
+
g.SetLimit(10) // max 10 in flight
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
## Channels — send, receive, close
|
|
98
|
+
|
|
99
|
+
```go
|
|
100
|
+
ch := make(chan int, 10) // buffered
|
|
101
|
+
|
|
102
|
+
go func() {
|
|
103
|
+
defer close(ch) // signal "no more values"
|
|
104
|
+
for i := 0; i < 5; i++ {
|
|
105
|
+
select {
|
|
106
|
+
case ch <- i:
|
|
107
|
+
case <-ctx.Done():
|
|
108
|
+
return // unblock on cancellation
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}()
|
|
112
|
+
|
|
113
|
+
for v := range ch { // exits when ch is closed
|
|
114
|
+
use(v)
|
|
115
|
+
}
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
Rules:
|
|
119
|
+
- **The sender closes**, never the receiver.
|
|
120
|
+
- Closing a nil or already-closed channel panics.
|
|
121
|
+
- Receive on a closed channel returns the zero value immediately; use `v, ok := <-ch` to detect close.
|
|
122
|
+
- A `send` on an unbuffered channel blocks until a receiver is ready. That's the synchronization.
|
|
123
|
+
|
|
124
|
+
## `select` — wait on multiple channels
|
|
125
|
+
|
|
126
|
+
```go
|
|
127
|
+
select {
|
|
128
|
+
case v := <-ch:
|
|
129
|
+
handle(v)
|
|
130
|
+
case <-ctx.Done():
|
|
131
|
+
return ctx.Err()
|
|
132
|
+
case <-time.After(1 * time.Second):
|
|
133
|
+
return fmt.Errorf("timeout")
|
|
134
|
+
}
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
`default:` makes `select` non-blocking:
|
|
138
|
+
```go
|
|
139
|
+
select {
|
|
140
|
+
case ch <- v:
|
|
141
|
+
default:
|
|
142
|
+
// drop; receiver not ready
|
|
143
|
+
}
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
## Fan-out, fan-in
|
|
147
|
+
|
|
148
|
+
```go
|
|
149
|
+
func pipeline(ctx context.Context, in <-chan T) <-chan R {
|
|
150
|
+
out := make(chan R)
|
|
151
|
+
var wg sync.WaitGroup
|
|
152
|
+
for i := 0; i < runtime.NumCPU(); i++ {
|
|
153
|
+
wg.Add(1)
|
|
154
|
+
go func() {
|
|
155
|
+
defer wg.Done()
|
|
156
|
+
for v := range in {
|
|
157
|
+
select {
|
|
158
|
+
case out <- process(v):
|
|
159
|
+
case <-ctx.Done():
|
|
160
|
+
return
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}()
|
|
164
|
+
}
|
|
165
|
+
go func() { wg.Wait(); close(out) }()
|
|
166
|
+
return out
|
|
167
|
+
}
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
Always ensure the downstream can drain — otherwise workers block on send forever after cancellation.
|
|
171
|
+
|
|
172
|
+
## Mutex vs. channels — pick the right tool
|
|
173
|
+
|
|
174
|
+
| Need | Use |
|
|
175
|
+
|---|---|
|
|
176
|
+
| Protect a shared map / counter / cache | `sync.Mutex` / `sync.RWMutex` |
|
|
177
|
+
| Protect a single value, read-heavy | `atomic.Value` / `sync/atomic` |
|
|
178
|
+
| Signal "done" / "stop" | close(chan) or `context` |
|
|
179
|
+
| Coordinate N workers on a job queue | channel as queue |
|
|
180
|
+
| One-time init | `sync.Once` |
|
|
181
|
+
| Pool of reusable objects (buffers) | `sync.Pool` |
|
|
182
|
+
|
|
183
|
+
Don't use channels for what a mutex does better (protect a map). Channels shine for flow and signalling.
|
|
184
|
+
|
|
185
|
+
## RWMutex
|
|
186
|
+
|
|
187
|
+
For read-heavy structures.
|
|
188
|
+
|
|
189
|
+
```go
|
|
190
|
+
type Cache struct {
|
|
191
|
+
mu sync.RWMutex
|
|
192
|
+
data map[string]V
|
|
193
|
+
}
|
|
194
|
+
func (c *Cache) Get(k string) (V, bool) {
|
|
195
|
+
c.mu.RLock(); defer c.mu.RUnlock()
|
|
196
|
+
v, ok := c.data[k]
|
|
197
|
+
return v, ok
|
|
198
|
+
}
|
|
199
|
+
func (c *Cache) Set(k string, v V) {
|
|
200
|
+
c.mu.Lock(); defer c.mu.Unlock()
|
|
201
|
+
c.data[k] = v
|
|
202
|
+
}
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
Don't use `RWMutex` if writes dominate; the overhead makes plain `Mutex` faster.
|
|
206
|
+
|
|
207
|
+
## `sync.Once`, `sync.Pool`
|
|
208
|
+
|
|
209
|
+
```go
|
|
210
|
+
var (
|
|
211
|
+
once sync.Once
|
|
212
|
+
cfg *Config
|
|
213
|
+
)
|
|
214
|
+
func getConfig() *Config {
|
|
215
|
+
once.Do(func() { cfg = loadConfig() })
|
|
216
|
+
return cfg
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
var bufPool = sync.Pool{ New: func() any { return new(bytes.Buffer) } }
|
|
220
|
+
func use() {
|
|
221
|
+
b := bufPool.Get().(*bytes.Buffer)
|
|
222
|
+
defer func() { b.Reset(); bufPool.Put(b) }()
|
|
223
|
+
...
|
|
224
|
+
}
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
`sync.Pool` for high-allocation-pressure paths. Don't pool everything — usually you're just adding complexity.
|
|
228
|
+
|
|
229
|
+
## Race detector
|
|
230
|
+
|
|
231
|
+
Run tests with `-race` in CI. It catches concurrent reads/writes that break invariants.
|
|
232
|
+
|
|
233
|
+
```
|
|
234
|
+
go test -race ./...
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
Cost: ~2-10× slowdown. Only for tests, not prod.
|
|
238
|
+
|
|
239
|
+
## Common patterns
|
|
240
|
+
|
|
241
|
+
### Timeout one operation
|
|
242
|
+
|
|
243
|
+
```go
|
|
244
|
+
ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
|
|
245
|
+
defer cancel()
|
|
246
|
+
result, err := slowOp(ctx)
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
### Heartbeat / leader lock renew
|
|
250
|
+
|
|
251
|
+
```go
|
|
252
|
+
t := time.NewTicker(15 * time.Second)
|
|
253
|
+
defer t.Stop()
|
|
254
|
+
for {
|
|
255
|
+
select {
|
|
256
|
+
case <-ctx.Done(): return
|
|
257
|
+
case <-t.C: renew()
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
### Worker pool with backpressure
|
|
263
|
+
|
|
264
|
+
```go
|
|
265
|
+
jobs := make(chan Job, 100)
|
|
266
|
+
for i := 0; i < 10; i++ {
|
|
267
|
+
go worker(ctx, jobs)
|
|
268
|
+
}
|
|
269
|
+
// producers send on jobs; buffer provides backpressure
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
## Anti-patterns
|
|
273
|
+
|
|
274
|
+
| Anti-pattern | Fix |
|
|
275
|
+
|---|---|
|
|
276
|
+
| `go f()` with no way to stop | Take `ctx.Context`; return on `<-ctx.Done()` |
|
|
277
|
+
| Reading from a channel with no timeout / cancel | `select { case v := <-ch: case <-ctx.Done(): }` |
|
|
278
|
+
| Closing a channel from the receiver | Only the sender closes |
|
|
279
|
+
| Sharing a `sync.Mutex` by value | Copy breaks locking — always by pointer |
|
|
280
|
+
| `for { select { default: ... } }` busy loop | Add a timer / cancel; don't spin |
|
|
281
|
+
| Global mutable maps without a mutex | Wrap in a type with a lock |
|
|
282
|
+
| Ignoring `context.Canceled` from HTTP / DB | Return it; it's usually correct |
|
|
283
|
+
| `time.Sleep` in request handlers | Use `time.After` inside `select` with `ctx.Done()` |
|
|
284
|
+
| Mixing `errgroup` and your own waitgroup | Pick one |
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
# Go — Errors
|
|
2
|
+
|
|
3
|
+
`error` interface, wrapping, `errors.Is/As`, sentinel vs. typed errors. For general strategy, see `coding-standards/references/error-strategy.md`.
|
|
4
|
+
|
|
5
|
+
## The `error` interface
|
|
6
|
+
|
|
7
|
+
```go
|
|
8
|
+
type error interface {
|
|
9
|
+
Error() string
|
|
10
|
+
}
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
That's it. Any type with an `Error() string` method is an error.
|
|
14
|
+
|
|
15
|
+
## Return errors; don't panic
|
|
16
|
+
|
|
17
|
+
Go signals failure by returning an error as the last value. Callers must handle it.
|
|
18
|
+
|
|
19
|
+
```go
|
|
20
|
+
f, err := os.Open(path)
|
|
21
|
+
if err != nil {
|
|
22
|
+
return fmt.Errorf("open %s: %w", path, err)
|
|
23
|
+
}
|
|
24
|
+
defer f.Close()
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Panic only for:
|
|
28
|
+
- Programmer errors (nil deref on a value you just constructed)
|
|
29
|
+
- Unrecoverable state (corrupted global, failed init on a must-have resource)
|
|
30
|
+
- `init()` failures
|
|
31
|
+
|
|
32
|
+
Never panic across a library boundary. Recover at the edge if you must, and log.
|
|
33
|
+
|
|
34
|
+
## Wrap with `%w`, not `%v` or `%s`
|
|
35
|
+
|
|
36
|
+
`fmt.Errorf` with `%w` preserves the underlying error for `errors.Is` / `errors.As`.
|
|
37
|
+
|
|
38
|
+
```go
|
|
39
|
+
// ✅
|
|
40
|
+
return fmt.Errorf("load config %q: %w", path, err)
|
|
41
|
+
|
|
42
|
+
// ❌ loses the original
|
|
43
|
+
return fmt.Errorf("load config %q: %v", path, err)
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Wrap at each layer you cross. The final error has a breadcrumb trail.
|
|
47
|
+
|
|
48
|
+
```
|
|
49
|
+
load config "/etc/app.yaml": parse yaml: invalid syntax at line 12
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## `errors.Is` — sentinel comparison
|
|
53
|
+
|
|
54
|
+
For "fixed" errors that the caller wants to match.
|
|
55
|
+
|
|
56
|
+
```go
|
|
57
|
+
var ErrNotFound = errors.New("not found")
|
|
58
|
+
|
|
59
|
+
func Find(id string) (*User, error) {
|
|
60
|
+
if ... { return nil, ErrNotFound }
|
|
61
|
+
...
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Caller
|
|
65
|
+
u, err := Find(id)
|
|
66
|
+
if errors.Is(err, ErrNotFound) {
|
|
67
|
+
return http.StatusNotFound, nil
|
|
68
|
+
}
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
`errors.Is` walks the wrap chain. Direct `==` comparison does not — use `errors.Is`.
|
|
72
|
+
|
|
73
|
+
## `errors.As` — typed errors with fields
|
|
74
|
+
|
|
75
|
+
When the caller needs data from the error (status code, field name), use a type with `errors.As`.
|
|
76
|
+
|
|
77
|
+
```go
|
|
78
|
+
type ValidationError struct {
|
|
79
|
+
Field string
|
|
80
|
+
Message string
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
func (e *ValidationError) Error() string {
|
|
84
|
+
return fmt.Sprintf("%s: %s", e.Field, e.Message)
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Caller
|
|
88
|
+
var ve *ValidationError
|
|
89
|
+
if errors.As(err, &ve) {
|
|
90
|
+
return fmt.Sprintf("field %s invalid", ve.Field)
|
|
91
|
+
}
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
`errors.As` walks the chain and binds to the first matching type.
|
|
95
|
+
|
|
96
|
+
## Sentinel vs. typed — when to use which
|
|
97
|
+
|
|
98
|
+
| Use a sentinel (`var ErrX = errors.New(...)`) | Use a typed error |
|
|
99
|
+
|---|---|
|
|
100
|
+
| Caller only needs to know "is it X?" | Caller needs details (field, code, retry-after) |
|
|
101
|
+
| Common across packages: `io.EOF`, `sql.ErrNoRows` | Validation failures, HTTP errors, domain errors |
|
|
102
|
+
| One concept, one value | Data varies per occurrence |
|
|
103
|
+
|
|
104
|
+
Prefer typed errors for anything the caller acts on with specific logic.
|
|
105
|
+
|
|
106
|
+
## Where to define errors
|
|
107
|
+
|
|
108
|
+
- **Sentinels**: in the package that can produce them. `package user`: `var ErrUserNotFound = errors.New("user not found")`.
|
|
109
|
+
- **Types**: same rule. Domain errors live in the domain package.
|
|
110
|
+
|
|
111
|
+
Don't put all errors in a shared `errors` package — it couples everything.
|
|
112
|
+
|
|
113
|
+
## Don't discard context
|
|
114
|
+
|
|
115
|
+
```go
|
|
116
|
+
// ❌ loses the cause
|
|
117
|
+
if err != nil {
|
|
118
|
+
return errors.New("something failed")
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// ❌ loses the cause and the stack
|
|
122
|
+
if err != nil {
|
|
123
|
+
log.Println("error:", err)
|
|
124
|
+
return nil
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// ✅
|
|
128
|
+
if err != nil {
|
|
129
|
+
return fmt.Errorf("load user %s: %w", id, err)
|
|
130
|
+
}
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
A wrapped error is dozens of times cheaper to debug than an unwrapped one.
|
|
134
|
+
|
|
135
|
+
## Check errors once, at the right place
|
|
136
|
+
|
|
137
|
+
```go
|
|
138
|
+
// ❌ stutter — log + rewrap + return
|
|
139
|
+
if err != nil {
|
|
140
|
+
log.Printf("load failed: %v", err)
|
|
141
|
+
return fmt.Errorf("load: %w", err)
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// ✅ let the caller decide
|
|
145
|
+
if err != nil {
|
|
146
|
+
return fmt.Errorf("load %s: %w", id, err)
|
|
147
|
+
}
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
Log where the error is **handled** (not re-thrown). Every rethrow-then-log doubles log volume.
|
|
151
|
+
|
|
152
|
+
## Edge: translate errors to HTTP status
|
|
153
|
+
|
|
154
|
+
```go
|
|
155
|
+
func writeError(w http.ResponseWriter, err error) {
|
|
156
|
+
var ve *ValidationError
|
|
157
|
+
switch {
|
|
158
|
+
case errors.As(err, &ve):
|
|
159
|
+
http.Error(w, ve.Error(), http.StatusUnprocessableEntity)
|
|
160
|
+
case errors.Is(err, ErrNotFound):
|
|
161
|
+
http.Error(w, "not found", http.StatusNotFound)
|
|
162
|
+
case errors.Is(err, ErrUnauthorized):
|
|
163
|
+
http.Error(w, "unauthorized", http.StatusUnauthorized)
|
|
164
|
+
default:
|
|
165
|
+
log.Printf("internal: %v", err)
|
|
166
|
+
http.Error(w, "internal error", http.StatusInternalServerError)
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
One translator, at the edge. Handlers just return errors.
|
|
172
|
+
|
|
173
|
+
## `errors.Join` — multiple errors at once
|
|
174
|
+
|
|
175
|
+
Go 1.20+: collect errors from a batch.
|
|
176
|
+
|
|
177
|
+
```go
|
|
178
|
+
var errs []error
|
|
179
|
+
for _, x := range xs {
|
|
180
|
+
if err := process(x); err != nil {
|
|
181
|
+
errs = append(errs, err)
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
if err := errors.Join(errs...); err != nil {
|
|
185
|
+
return err
|
|
186
|
+
}
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
`errors.Is` / `errors.As` work across joined errors.
|
|
190
|
+
|
|
191
|
+
## `defer` + named returns for cleanup errors
|
|
192
|
+
|
|
193
|
+
When `Close` can fail and you want to surface it:
|
|
194
|
+
|
|
195
|
+
```go
|
|
196
|
+
func write(path string, data []byte) (err error) {
|
|
197
|
+
f, err := os.Create(path)
|
|
198
|
+
if err != nil { return err }
|
|
199
|
+
defer func() {
|
|
200
|
+
if cerr := f.Close(); cerr != nil && err == nil {
|
|
201
|
+
err = cerr
|
|
202
|
+
}
|
|
203
|
+
}()
|
|
204
|
+
_, err = f.Write(data)
|
|
205
|
+
return err
|
|
206
|
+
}
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
Named return `err` is read by the deferred func. Use sparingly — only when the cleanup error matters.
|
|
210
|
+
|
|
211
|
+
## `panic` / `recover` — edge cases only
|
|
212
|
+
|
|
213
|
+
```go
|
|
214
|
+
// One legitimate use: HTTP middleware recovering a goroutine panic
|
|
215
|
+
func recoverMW(next http.Handler) http.Handler {
|
|
216
|
+
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
217
|
+
defer func() {
|
|
218
|
+
if rv := recover(); rv != nil {
|
|
219
|
+
log.Printf("panic: %v\n%s", rv, debug.Stack())
|
|
220
|
+
http.Error(w, "internal error", http.StatusInternalServerError)
|
|
221
|
+
}
|
|
222
|
+
}()
|
|
223
|
+
next.ServeHTTP(w, r)
|
|
224
|
+
})
|
|
225
|
+
}
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
Everywhere else: return `error`. `recover` is a safety net, not a control-flow tool.
|
|
229
|
+
|
|
230
|
+
## Anti-patterns
|
|
231
|
+
|
|
232
|
+
| Anti-pattern | Fix |
|
|
233
|
+
|---|---|
|
|
234
|
+
| `if err != nil { return err }` with no context | Wrap: `fmt.Errorf("op: %w", err)` |
|
|
235
|
+
| `errors.New("failed")` at every layer — no detail | Include the inputs and the operation |
|
|
236
|
+
| Returning `nil` error but also `nil` result | Decide the contract; don't force callers to guess |
|
|
237
|
+
| `recover` to "keep the server running" through bugs | Fix the bug; panics are bugs |
|
|
238
|
+
| String comparison on error messages | `errors.Is` / `errors.As` |
|
|
239
|
+
| Single huge `Error` struct with a `.Code` field | Use typed errors + `errors.As` |
|
|
240
|
+
| `log.Fatal` / `os.Exit` from deep inside libraries | Return; let `main` decide |
|
|
241
|
+
| `_ = someErrorReturningCall()` without a comment | Either handle or document the reason |
|