@coralai/sps-cli 0.42.0 → 0.44.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.
Files changed (147) hide show
  1. package/README.md +59 -4
  2. package/dist/commands/consoleCommand.d.ts +2 -0
  3. package/dist/commands/consoleCommand.d.ts.map +1 -0
  4. package/dist/commands/consoleCommand.js +129 -0
  5. package/dist/commands/consoleCommand.js.map +1 -0
  6. package/dist/commands/projectInit.d.ts.map +1 -1
  7. package/dist/commands/projectInit.js +40 -53
  8. package/dist/commands/projectInit.js.map +1 -1
  9. package/dist/commands/setup.d.ts.map +1 -1
  10. package/dist/commands/setup.js +14 -2
  11. package/dist/commands/setup.js.map +1 -1
  12. package/dist/commands/skillCommand.d.ts +2 -0
  13. package/dist/commands/skillCommand.d.ts.map +1 -0
  14. package/dist/commands/skillCommand.js +235 -0
  15. package/dist/commands/skillCommand.js.map +1 -0
  16. package/dist/console-assets/assets/index-Bhd2f9AP.js +125 -0
  17. package/dist/console-assets/assets/index-bsAN2a12.css +1 -0
  18. package/dist/console-assets/index.html +16 -0
  19. package/dist/console-server/index.d.ts +29 -0
  20. package/dist/console-server/index.d.ts.map +1 -0
  21. package/dist/console-server/index.js +145 -0
  22. package/dist/console-server/index.js.map +1 -0
  23. package/dist/console-server/lib/lockFile.d.ts +17 -0
  24. package/dist/console-server/lib/lockFile.d.ts.map +1 -0
  25. package/dist/console-server/lib/lockFile.js +61 -0
  26. package/dist/console-server/lib/lockFile.js.map +1 -0
  27. package/dist/console-server/lib/portPicker.d.ts +3 -0
  28. package/dist/console-server/lib/portPicker.d.ts.map +1 -0
  29. package/dist/console-server/lib/portPicker.js +25 -0
  30. package/dist/console-server/lib/portPicker.js.map +1 -0
  31. package/dist/console-server/routes/projects.d.ts +11 -0
  32. package/dist/console-server/routes/projects.d.ts.map +1 -0
  33. package/dist/console-server/routes/projects.js +149 -0
  34. package/dist/console-server/routes/projects.js.map +1 -0
  35. package/dist/console-server/routes/system.d.ts +7 -0
  36. package/dist/console-server/routes/system.d.ts.map +1 -0
  37. package/dist/console-server/routes/system.js +19 -0
  38. package/dist/console-server/routes/system.js.map +1 -0
  39. package/dist/console-server/sse/eventBus.d.ts +25 -0
  40. package/dist/console-server/sse/eventBus.d.ts.map +1 -0
  41. package/dist/console-server/sse/eventBus.js +32 -0
  42. package/dist/console-server/sse/eventBus.js.map +1 -0
  43. package/dist/console-server/watchers/cardWatcher.d.ts +9 -0
  44. package/dist/console-server/watchers/cardWatcher.d.ts.map +1 -0
  45. package/dist/console-server/watchers/cardWatcher.js +42 -0
  46. package/dist/console-server/watchers/cardWatcher.js.map +1 -0
  47. package/dist/core/skillStore.d.ts +46 -0
  48. package/dist/core/skillStore.d.ts.map +1 -0
  49. package/dist/core/skillStore.js +210 -0
  50. package/dist/core/skillStore.js.map +1 -0
  51. package/dist/core/skillStore.test.d.ts +2 -0
  52. package/dist/core/skillStore.test.d.ts.map +1 -0
  53. package/dist/core/skillStore.test.js +203 -0
  54. package/dist/core/skillStore.test.js.map +1 -0
  55. package/dist/main.js +27 -17
  56. package/dist/main.js.map +1 -1
  57. package/package.json +8 -2
  58. package/skills/architecture-decision-records/SKILL.md +207 -0
  59. package/skills/backend/SKILL.md +62 -0
  60. package/skills/backend/references/api-design.md +168 -0
  61. package/skills/backend/references/caching.md +181 -0
  62. package/skills/backend/references/data-access.md +173 -0
  63. package/skills/backend/references/layering.md +181 -0
  64. package/skills/backend/references/observability.md +190 -0
  65. package/skills/backend/references/resilience.md +201 -0
  66. package/skills/backend/references/security.md +186 -0
  67. package/skills/backend-architect/SKILL.md +119 -0
  68. package/skills/code-reviewer/SKILL.md +143 -0
  69. package/skills/coding-standards/SKILL.md +60 -0
  70. package/skills/coding-standards/references/clean-code.md +258 -0
  71. package/skills/coding-standards/references/code-review.md +192 -0
  72. package/skills/coding-standards/references/commits-and-prs.md +226 -0
  73. package/skills/coding-standards/references/error-strategy.md +193 -0
  74. package/skills/coding-standards/references/naming.md +185 -0
  75. package/skills/coding-standards/references/tdd.md +171 -0
  76. package/skills/database/SKILL.md +53 -0
  77. package/skills/database/references/indexing.md +190 -0
  78. package/skills/database/references/migrations.md +199 -0
  79. package/skills/database/references/nosql.md +185 -0
  80. package/skills/database/references/queries.md +295 -0
  81. package/skills/database/references/scaling.md +203 -0
  82. package/skills/database/references/schema.md +191 -0
  83. package/skills/database-optimizer/SKILL.md +168 -0
  84. package/skills/debugging-workflow/SKILL.md +244 -0
  85. package/skills/devops/SKILL.md +55 -0
  86. package/skills/devops/references/ci-cd.md +204 -0
  87. package/skills/devops/references/containers.md +272 -0
  88. package/skills/devops/references/deploy.md +201 -0
  89. package/skills/devops/references/iac.md +252 -0
  90. package/skills/devops/references/observability.md +228 -0
  91. package/skills/devops/references/secrets.md +178 -0
  92. package/skills/devops-automator/SKILL.md +164 -0
  93. package/skills/frontend/SKILL.md +52 -0
  94. package/skills/frontend/references/accessibility.md +222 -0
  95. package/skills/frontend/references/components.md +206 -0
  96. package/skills/frontend/references/performance.md +219 -0
  97. package/skills/frontend/references/routing.md +209 -0
  98. package/skills/frontend/references/state.md +190 -0
  99. package/skills/frontend/references/testing.md +216 -0
  100. package/skills/frontend-developer/SKILL.md +115 -0
  101. package/skills/git-workflow/SKILL.md +355 -0
  102. package/skills/golang/SKILL.md +49 -0
  103. package/skills/golang/references/concurrency.md +284 -0
  104. package/skills/golang/references/errors.md +241 -0
  105. package/skills/golang/references/idioms.md +285 -0
  106. package/skills/golang/references/testing.md +238 -0
  107. package/skills/java/SKILL.md +50 -0
  108. package/skills/java/references/concurrency.md +194 -0
  109. package/skills/java/references/idioms.md +283 -0
  110. package/skills/java/references/testing.md +228 -0
  111. package/skills/kotlin/SKILL.md +47 -0
  112. package/skills/kotlin/references/coroutines.md +240 -0
  113. package/skills/kotlin/references/idioms.md +268 -0
  114. package/skills/kotlin/references/testing.md +219 -0
  115. package/skills/mobile/SKILL.md +50 -0
  116. package/skills/mobile/references/architecture.md +204 -0
  117. package/skills/mobile/references/navigation.md +158 -0
  118. package/skills/mobile/references/performance.md +152 -0
  119. package/skills/mobile/references/platform.md +166 -0
  120. package/skills/mobile/references/state-and-data.md +174 -0
  121. package/skills/python/SKILL.md +51 -0
  122. package/skills/python/THIRD_PARTY.md +14 -0
  123. package/skills/python/references/async.md +218 -0
  124. package/skills/python/references/error-handling.md +254 -0
  125. package/skills/python/references/idioms.md +279 -0
  126. package/skills/python/references/packaging.md +233 -0
  127. package/skills/python/references/testing.md +269 -0
  128. package/skills/python/references/typing.md +292 -0
  129. package/skills/qa-tester/SKILL.md +186 -0
  130. package/skills/rust/SKILL.md +50 -0
  131. package/skills/rust/references/async.md +224 -0
  132. package/skills/rust/references/errors.md +240 -0
  133. package/skills/rust/references/ownership.md +263 -0
  134. package/skills/rust/references/testing.md +274 -0
  135. package/skills/rust/references/traits.md +250 -0
  136. package/skills/security-engineer/SKILL.md +157 -0
  137. package/skills/swift/SKILL.md +48 -0
  138. package/skills/swift/references/concurrency.md +280 -0
  139. package/skills/swift/references/idioms.md +334 -0
  140. package/skills/swift/references/testing.md +229 -0
  141. package/skills/typescript/SKILL.md +51 -0
  142. package/skills/typescript/references/async.md +241 -0
  143. package/skills/typescript/references/errors.md +208 -0
  144. package/skills/typescript/references/idioms.md +246 -0
  145. package/skills/typescript/references/testing.md +225 -0
  146. package/skills/typescript/references/tooling.md +208 -0
  147. 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 |