@baselane/packs 0.1.3 → 0.1.5
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/dist/aliases.js +1 -0
- package/package.json +2 -2
- package/packs/database-review/pack.json +75 -9
- package/packs/database-review/rendered/.claude/agents/database-reviewer.md +75 -2
- package/packs/database-review/rendered/.claude/commands/migration-review.md +6 -0
- package/packs/database-review/rendered/.claude/settings.json +11 -0
- package/packs/database-review/rendered/.claude/skills/database-migrations/SKILL.md +431 -0
- package/packs/database-review/rendered/.claude/skills/mysql-patterns/SKILL.md +414 -0
- package/packs/database-review/rendered/.claude/skills/postgres-patterns/SKILL.md +149 -0
- package/packs/database-review/rendered/.github/copilot-instructions.md +1096 -14
- package/packs/database-review/rendered/AGENTS.md +45 -16
- package/packs/database-review/rendered/CLAUDE.md +4 -0
- package/packs/database-review/rendered/GEMINI.md +987 -0
- package/packs/go-rules/pack.json +65 -9
- package/packs/go-rules/rendered/.claude/agents/go-build-resolver.md +90 -0
- package/packs/go-rules/rendered/.claude/agents/go-reviewer.md +72 -2
- package/packs/go-rules/rendered/.claude/commands/go-build.md +6 -0
- package/packs/go-rules/rendered/.claude/commands/go-review.md +1 -1
- package/packs/go-rules/rendered/.claude/commands/go-test.md +6 -0
- package/packs/go-rules/rendered/.claude/settings.json +9 -0
- package/packs/go-rules/rendered/.claude/skills/golang-patterns/SKILL.md +673 -0
- package/packs/go-rules/rendered/.claude/skills/golang-testing/SKILL.md +719 -0
- package/packs/go-rules/rendered/.github/copilot-instructions.md +1783 -7
- package/packs/go-rules/rendered/AGENTS.md +244 -8
- package/packs/go-rules/rendered/CLAUDE.md +5 -0
- package/packs/go-rules/rendered/GEMINI.md +1394 -0
- package/packs/python-rules/pack.json +87 -10
- package/packs/python-rules/rendered/.claude/agents/django-reviewer.md +159 -0
- package/packs/python-rules/rendered/.claude/agents/fastapi-reviewer.md +70 -0
- package/packs/python-rules/rendered/.claude/agents/python-reviewer.md +92 -2
- package/packs/python-rules/rendered/.claude/commands/django-review.md +6 -0
- package/packs/python-rules/rendered/.claude/commands/fastapi-review.md +6 -0
- package/packs/python-rules/rendered/.claude/commands/python-review.md +1 -1
- package/packs/python-rules/rendered/.claude/settings.json +1 -1
- package/packs/python-rules/rendered/.claude/skills/django-security/SKILL.md +642 -0
- package/packs/python-rules/rendered/.claude/skills/django-tdd/SKILL.md +728 -0
- package/packs/python-rules/rendered/.claude/skills/fastapi-patterns/SKILL.md +512 -0
- package/packs/python-rules/rendered/.claude/skills/python-patterns/SKILL.md +749 -0
- package/packs/python-rules/rendered/.claude/skills/python-testing/SKILL.md +815 -0
- package/packs/python-rules/rendered/.github/copilot-instructions.md +3815 -12
- package/packs/python-rules/rendered/AGENTS.md +73 -13
- package/packs/python-rules/rendered/CLAUDE.md +9 -0
- package/packs/python-rules/rendered/GEMINI.md +3448 -0
- package/packs/security-review/pack.json +51 -7
- package/packs/security-review/rendered/.claude/agents/security-reviewer.md +29 -2
- package/packs/security-review/rendered/.claude/settings.json +11 -0
- package/packs/security-review/rendered/.claude/skills/security-review/SKILL.md +302 -0
- package/packs/security-review/rendered/.claude/skills/security-review/cloud-infrastructure-security.md +171 -0
- package/packs/security-review/rendered/.claude/skills/security-scan-config/SKILL.md +56 -0
- package/packs/security-review/rendered/.github/copilot-instructions.md +658 -15
- package/packs/security-review/rendered/AGENTS.md +283 -17
- package/packs/security-review/rendered/CLAUDE.md +2 -0
- package/packs/security-review/rendered/GEMINI.md +352 -0
- package/packs/software-engineer-harness/pack.json +249 -13
- package/packs/software-engineer-harness/rendered/.claude/agents/build-error-resolver.md +15 -0
- package/packs/software-engineer-harness/rendered/.claude/agents/code-explorer.md +67 -0
- package/packs/software-engineer-harness/rendered/.claude/agents/performance-optimizer.md +444 -0
- package/packs/software-engineer-harness/rendered/.claude/agents/pr-test-analyzer.md +43 -0
- package/packs/software-engineer-harness/rendered/.claude/agents/refactor-cleaner.md +12 -0
- package/packs/software-engineer-harness/rendered/.claude/agents/silent-failure-hunter.md +16 -0
- package/packs/software-engineer-harness/rendered/.claude/agents/tdd-guide.md +16 -0
- package/packs/software-engineer-harness/rendered/.claude/commands/feature-dev.md +6 -0
- package/packs/software-engineer-harness/rendered/.claude/commands/fix-build.md +6 -0
- package/packs/software-engineer-harness/rendered/.claude/commands/hunt-silent-failures.md +6 -0
- package/packs/software-engineer-harness/rendered/.claude/commands/tdd.md +6 -0
- package/packs/software-engineer-harness/rendered/.claude/settings.json +29 -0
- package/packs/software-engineer-harness/rendered/.claude/skills/api-design/SKILL.md +522 -0
- package/packs/software-engineer-harness/rendered/.claude/skills/architecture-decision-records/SKILL.md +178 -0
- package/packs/software-engineer-harness/rendered/.claude/skills/backend-patterns/SKILL.md +560 -0
- package/packs/software-engineer-harness/rendered/.claude/skills/codebase-onboarding/SKILL.md +232 -0
- package/packs/software-engineer-harness/rendered/.claude/skills/deployment-patterns/SKILL.md +426 -0
- package/packs/software-engineer-harness/rendered/.claude/skills/docker-patterns/SKILL.md +363 -0
- package/packs/software-engineer-harness/rendered/.claude/skills/e2e-testing/SKILL.md +325 -0
- package/packs/software-engineer-harness/rendered/.claude/skills/tdd-red-green-refactor/SKILL.md +20 -0
- package/packs/software-engineer-harness/rendered/.claude/skills/tdd-workflow/SKILL.md +580 -0
- package/packs/software-engineer-harness/rendered/.github/copilot-instructions.md +3948 -2
- package/packs/software-engineer-harness/rendered/AGENTS.md +194 -4
- package/packs/software-engineer-harness/rendered/CLAUDE.md +20 -0
- package/packs/software-engineer-harness/rendered/GEMINI.md +3208 -0
- package/packs/typescript-rules/pack.json +68 -5
- package/packs/typescript-rules/rendered/.claude/agents/react-reviewer.md +165 -0
- package/packs/typescript-rules/rendered/.claude/agents/type-design-analyzer.md +48 -0
- package/packs/typescript-rules/rendered/.claude/commands/react-review.md +6 -0
- package/packs/typescript-rules/rendered/.claude/settings.json +9 -0
- package/packs/typescript-rules/rendered/.claude/skills/react-patterns/SKILL.md +340 -0
- package/packs/typescript-rules/rendered/.claude/skills/react-performance/SKILL.md +573 -0
- package/packs/typescript-rules/rendered/.claude/skills/react-testing/SKILL.md +422 -0
- package/packs/typescript-rules/rendered/.github/copilot-instructions.md +2459 -53
- package/packs/typescript-rules/rendered/AGENTS.md +893 -17
- package/packs/typescript-rules/rendered/CLAUDE.md +6 -0
- package/packs/typescript-rules/rendered/GEMINI.md +1335 -0
|
@@ -1,3 +1,1397 @@
|
|
|
1
1
|
# GEMINI.md
|
|
2
2
|
|
|
3
3
|
@AGENTS.md
|
|
4
|
+
|
|
5
|
+
## Skills
|
|
6
|
+
|
|
7
|
+
### golang-patterns
|
|
8
|
+
|
|
9
|
+
Idiomatic Go patterns, best practices, and conventions for building robust, efficient, and maintainable Go applications — API shape, error handling, concurrency, interface design, package organization, and performance. Use when writing, reviewing, or refactoring Go code, or designing Go packages/modules.
|
|
10
|
+
|
|
11
|
+
# Go Development Patterns
|
|
12
|
+
|
|
13
|
+
Idiomatic Go patterns and best practices for building robust, efficient, and maintainable applications.
|
|
14
|
+
|
|
15
|
+
## When to use
|
|
16
|
+
|
|
17
|
+
- Writing new Go code.
|
|
18
|
+
- Reviewing Go code.
|
|
19
|
+
- Refactoring existing Go code.
|
|
20
|
+
- Designing Go packages/modules.
|
|
21
|
+
|
|
22
|
+
## Core principles
|
|
23
|
+
|
|
24
|
+
### 1. Simplicity and clarity
|
|
25
|
+
|
|
26
|
+
Go favors simplicity over cleverness. Code should be obvious and easy to read.
|
|
27
|
+
|
|
28
|
+
\`\`\`go
|
|
29
|
+
// Good: clear and direct
|
|
30
|
+
func GetUser(id string) (*User, error) {
|
|
31
|
+
user, err := db.FindUser(id)
|
|
32
|
+
if err != nil {
|
|
33
|
+
return nil, fmt.Errorf("get user %s: %w", id, err)
|
|
34
|
+
}
|
|
35
|
+
return user, nil
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Bad: overly clever
|
|
39
|
+
func GetUser(id string) (*User, error) {
|
|
40
|
+
return func() (*User, error) {
|
|
41
|
+
if u, e := db.FindUser(id); e == nil {
|
|
42
|
+
return u, nil
|
|
43
|
+
} else {
|
|
44
|
+
return nil, e
|
|
45
|
+
}
|
|
46
|
+
}()
|
|
47
|
+
}
|
|
48
|
+
\`\`\`
|
|
49
|
+
|
|
50
|
+
### 2. Make the zero value useful
|
|
51
|
+
|
|
52
|
+
Design types so their zero value is immediately usable without initialization.
|
|
53
|
+
|
|
54
|
+
\`\`\`go
|
|
55
|
+
// Good: zero value is useful
|
|
56
|
+
type Counter struct {
|
|
57
|
+
mu sync.Mutex
|
|
58
|
+
count int // zero value is 0, ready to use
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
func (c *Counter) Inc() {
|
|
62
|
+
c.mu.Lock()
|
|
63
|
+
c.count++
|
|
64
|
+
c.mu.Unlock()
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Good: bytes.Buffer works with zero value
|
|
68
|
+
var buf bytes.Buffer
|
|
69
|
+
buf.WriteString("hello")
|
|
70
|
+
|
|
71
|
+
// Bad: requires initialization
|
|
72
|
+
type BadCounter struct {
|
|
73
|
+
counts map[string]int // nil map will panic
|
|
74
|
+
}
|
|
75
|
+
\`\`\`
|
|
76
|
+
|
|
77
|
+
### 3. Accept interfaces, return structs
|
|
78
|
+
|
|
79
|
+
Functions should accept interface parameters and return concrete types.
|
|
80
|
+
|
|
81
|
+
\`\`\`go
|
|
82
|
+
// Good: accepts interface, returns concrete type
|
|
83
|
+
func ProcessData(r io.Reader) (*Result, error) {
|
|
84
|
+
data, err := io.ReadAll(r)
|
|
85
|
+
if err != nil {
|
|
86
|
+
return nil, err
|
|
87
|
+
}
|
|
88
|
+
return &Result{Data: data}, nil
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Bad: returns interface (hides implementation details unnecessarily)
|
|
92
|
+
func ProcessData(r io.Reader) (io.Reader, error) {
|
|
93
|
+
// ...
|
|
94
|
+
}
|
|
95
|
+
\`\`\`
|
|
96
|
+
|
|
97
|
+
## Error handling patterns
|
|
98
|
+
|
|
99
|
+
### Error wrapping with context
|
|
100
|
+
|
|
101
|
+
\`\`\`go
|
|
102
|
+
func LoadConfig(path string) (*Config, error) {
|
|
103
|
+
data, err := os.ReadFile(path)
|
|
104
|
+
if err != nil {
|
|
105
|
+
return nil, fmt.Errorf("load config %s: %w", path, err)
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
var cfg Config
|
|
109
|
+
if err := json.Unmarshal(data, &cfg); err != nil {
|
|
110
|
+
return nil, fmt.Errorf("parse config %s: %w", path, err)
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return &cfg, nil
|
|
114
|
+
}
|
|
115
|
+
\`\`\`
|
|
116
|
+
|
|
117
|
+
### Custom error types
|
|
118
|
+
|
|
119
|
+
\`\`\`go
|
|
120
|
+
// Define domain-specific errors
|
|
121
|
+
type ValidationError struct {
|
|
122
|
+
Field string
|
|
123
|
+
Message string
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
func (e *ValidationError) Error() string {
|
|
127
|
+
return fmt.Sprintf("validation failed on %s: %s", e.Field, e.Message)
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Sentinel errors for common cases
|
|
131
|
+
var (
|
|
132
|
+
ErrNotFound = errors.New("resource not found")
|
|
133
|
+
ErrUnauthorized = errors.New("unauthorized")
|
|
134
|
+
ErrInvalidInput = errors.New("invalid input")
|
|
135
|
+
)
|
|
136
|
+
\`\`\`
|
|
137
|
+
|
|
138
|
+
### Error checking with errors.Is and errors.As
|
|
139
|
+
|
|
140
|
+
\`\`\`go
|
|
141
|
+
func HandleError(err error) {
|
|
142
|
+
// Check for specific error
|
|
143
|
+
if errors.Is(err, sql.ErrNoRows) {
|
|
144
|
+
log.Println("No records found")
|
|
145
|
+
return
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Check for error type
|
|
149
|
+
var validationErr *ValidationError
|
|
150
|
+
if errors.As(err, &validationErr) {
|
|
151
|
+
log.Printf("Validation error on field %s: %s",
|
|
152
|
+
validationErr.Field, validationErr.Message)
|
|
153
|
+
return
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Unknown error
|
|
157
|
+
log.Printf("Unexpected error: %v", err)
|
|
158
|
+
}
|
|
159
|
+
\`\`\`
|
|
160
|
+
|
|
161
|
+
### Never ignore errors
|
|
162
|
+
|
|
163
|
+
\`\`\`go
|
|
164
|
+
// Bad: ignoring error with blank identifier
|
|
165
|
+
result, _ := doSomething()
|
|
166
|
+
|
|
167
|
+
// Good: handle or explicitly document why it's safe to ignore
|
|
168
|
+
result, err := doSomething()
|
|
169
|
+
if err != nil {
|
|
170
|
+
return err
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Acceptable: when error truly doesn't matter (rare)
|
|
174
|
+
_ = writer.Close() // Best-effort cleanup, error logged elsewhere
|
|
175
|
+
\`\`\`
|
|
176
|
+
|
|
177
|
+
## Concurrency patterns
|
|
178
|
+
|
|
179
|
+
### Worker pool
|
|
180
|
+
|
|
181
|
+
\`\`\`go
|
|
182
|
+
func WorkerPool(jobs <-chan Job, results chan<- Result, numWorkers int) {
|
|
183
|
+
var wg sync.WaitGroup
|
|
184
|
+
|
|
185
|
+
for i := 0; i < numWorkers; i++ {
|
|
186
|
+
wg.Add(1)
|
|
187
|
+
go func() {
|
|
188
|
+
defer wg.Done()
|
|
189
|
+
for job := range jobs {
|
|
190
|
+
results <- process(job)
|
|
191
|
+
}
|
|
192
|
+
}()
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
wg.Wait()
|
|
196
|
+
close(results)
|
|
197
|
+
}
|
|
198
|
+
\`\`\`
|
|
199
|
+
|
|
200
|
+
### Context for cancellation and timeouts
|
|
201
|
+
|
|
202
|
+
\`\`\`go
|
|
203
|
+
func FetchWithTimeout(ctx context.Context, url string) ([]byte, error) {
|
|
204
|
+
ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
|
|
205
|
+
defer cancel()
|
|
206
|
+
|
|
207
|
+
req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
|
|
208
|
+
if err != nil {
|
|
209
|
+
return nil, fmt.Errorf("create request: %w", err)
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
resp, err := http.DefaultClient.Do(req)
|
|
213
|
+
if err != nil {
|
|
214
|
+
return nil, fmt.Errorf("fetch %s: %w", url, err)
|
|
215
|
+
}
|
|
216
|
+
defer resp.Body.Close()
|
|
217
|
+
|
|
218
|
+
return io.ReadAll(resp.Body)
|
|
219
|
+
}
|
|
220
|
+
\`\`\`
|
|
221
|
+
|
|
222
|
+
### Graceful shutdown
|
|
223
|
+
|
|
224
|
+
\`\`\`go
|
|
225
|
+
func GracefulShutdown(server *http.Server) {
|
|
226
|
+
quit := make(chan os.Signal, 1)
|
|
227
|
+
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
|
|
228
|
+
|
|
229
|
+
<-quit
|
|
230
|
+
log.Println("Shutting down server...")
|
|
231
|
+
|
|
232
|
+
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
|
233
|
+
defer cancel()
|
|
234
|
+
|
|
235
|
+
if err := server.Shutdown(ctx); err != nil {
|
|
236
|
+
log.Fatalf("Server forced to shutdown: %v", err)
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
log.Println("Server exited")
|
|
240
|
+
}
|
|
241
|
+
\`\`\`
|
|
242
|
+
|
|
243
|
+
### errgroup for coordinated goroutines
|
|
244
|
+
|
|
245
|
+
\`\`\`go
|
|
246
|
+
import "golang.org/x/sync/errgroup"
|
|
247
|
+
|
|
248
|
+
func FetchAll(ctx context.Context, urls []string) ([][]byte, error) {
|
|
249
|
+
g, ctx := errgroup.WithContext(ctx)
|
|
250
|
+
results := make([][]byte, len(urls))
|
|
251
|
+
|
|
252
|
+
for i, url := range urls {
|
|
253
|
+
i, url := i, url // Capture loop variables
|
|
254
|
+
g.Go(func() error {
|
|
255
|
+
data, err := FetchWithTimeout(ctx, url)
|
|
256
|
+
if err != nil {
|
|
257
|
+
return err
|
|
258
|
+
}
|
|
259
|
+
results[i] = data
|
|
260
|
+
return nil
|
|
261
|
+
})
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
if err := g.Wait(); err != nil {
|
|
265
|
+
return nil, err
|
|
266
|
+
}
|
|
267
|
+
return results, nil
|
|
268
|
+
}
|
|
269
|
+
\`\`\`
|
|
270
|
+
|
|
271
|
+
### Avoiding goroutine leaks
|
|
272
|
+
|
|
273
|
+
\`\`\`go
|
|
274
|
+
// Bad: goroutine leak if context is cancelled
|
|
275
|
+
func leakyFetch(ctx context.Context, url string) <-chan []byte {
|
|
276
|
+
ch := make(chan []byte)
|
|
277
|
+
go func() {
|
|
278
|
+
data, _ := fetch(url)
|
|
279
|
+
ch <- data // Blocks forever if no receiver
|
|
280
|
+
}()
|
|
281
|
+
return ch
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// Good: properly handles cancellation
|
|
285
|
+
func safeFetch(ctx context.Context, url string) <-chan []byte {
|
|
286
|
+
ch := make(chan []byte, 1) // Buffered channel
|
|
287
|
+
go func() {
|
|
288
|
+
data, err := fetch(url)
|
|
289
|
+
if err != nil {
|
|
290
|
+
return
|
|
291
|
+
}
|
|
292
|
+
select {
|
|
293
|
+
case ch <- data:
|
|
294
|
+
case <-ctx.Done():
|
|
295
|
+
}
|
|
296
|
+
}()
|
|
297
|
+
return ch
|
|
298
|
+
}
|
|
299
|
+
\`\`\`
|
|
300
|
+
|
|
301
|
+
## Interface design
|
|
302
|
+
|
|
303
|
+
### Small, focused interfaces
|
|
304
|
+
|
|
305
|
+
\`\`\`go
|
|
306
|
+
// Good: single-method interfaces
|
|
307
|
+
type Reader interface {
|
|
308
|
+
Read(p []byte) (n int, err error)
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
type Writer interface {
|
|
312
|
+
Write(p []byte) (n int, err error)
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
type Closer interface {
|
|
316
|
+
Close() error
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// Compose interfaces as needed
|
|
320
|
+
type ReadWriteCloser interface {
|
|
321
|
+
Reader
|
|
322
|
+
Writer
|
|
323
|
+
Closer
|
|
324
|
+
}
|
|
325
|
+
\`\`\`
|
|
326
|
+
|
|
327
|
+
### Define interfaces where they're used
|
|
328
|
+
|
|
329
|
+
\`\`\`go
|
|
330
|
+
// In the consumer package, not the provider
|
|
331
|
+
package service
|
|
332
|
+
|
|
333
|
+
// UserStore defines what this service needs
|
|
334
|
+
type UserStore interface {
|
|
335
|
+
GetUser(id string) (*User, error)
|
|
336
|
+
SaveUser(user *User) error
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
type Service struct {
|
|
340
|
+
store UserStore
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// Concrete implementation can be in another package
|
|
344
|
+
// It doesn't need to know about this interface
|
|
345
|
+
\`\`\`
|
|
346
|
+
|
|
347
|
+
### Optional behavior with type assertions
|
|
348
|
+
|
|
349
|
+
\`\`\`go
|
|
350
|
+
type Flusher interface {
|
|
351
|
+
Flush() error
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
func WriteAndFlush(w io.Writer, data []byte) error {
|
|
355
|
+
if _, err := w.Write(data); err != nil {
|
|
356
|
+
return err
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
// Flush if supported
|
|
360
|
+
if f, ok := w.(Flusher); ok {
|
|
361
|
+
return f.Flush()
|
|
362
|
+
}
|
|
363
|
+
return nil
|
|
364
|
+
}
|
|
365
|
+
\`\`\`
|
|
366
|
+
|
|
367
|
+
## Package organization
|
|
368
|
+
|
|
369
|
+
### Standard project layout
|
|
370
|
+
|
|
371
|
+
\`\`\`text
|
|
372
|
+
myproject/
|
|
373
|
+
├── cmd/
|
|
374
|
+
│ └── myapp/
|
|
375
|
+
│ └── main.go # Entry point
|
|
376
|
+
├── internal/
|
|
377
|
+
│ ├── handler/ # HTTP handlers
|
|
378
|
+
│ ├── service/ # Business logic
|
|
379
|
+
│ ├── repository/ # Data access
|
|
380
|
+
│ └── config/ # Configuration
|
|
381
|
+
├── pkg/
|
|
382
|
+
│ └── client/ # Public API client
|
|
383
|
+
├── api/
|
|
384
|
+
│ └── v1/ # API definitions (proto, OpenAPI)
|
|
385
|
+
├── testdata/ # Test fixtures
|
|
386
|
+
├── go.mod
|
|
387
|
+
├── go.sum
|
|
388
|
+
└── Makefile
|
|
389
|
+
\`\`\`
|
|
390
|
+
|
|
391
|
+
### Package naming
|
|
392
|
+
|
|
393
|
+
\`\`\`go
|
|
394
|
+
// Good: short, lowercase, no underscores
|
|
395
|
+
package http
|
|
396
|
+
package json
|
|
397
|
+
package user
|
|
398
|
+
|
|
399
|
+
// Bad: verbose, mixed case, or redundant
|
|
400
|
+
package httpHandler
|
|
401
|
+
package json_parser
|
|
402
|
+
package userService // Redundant 'Service' suffix
|
|
403
|
+
\`\`\`
|
|
404
|
+
|
|
405
|
+
### Avoid package-level state
|
|
406
|
+
|
|
407
|
+
\`\`\`go
|
|
408
|
+
// Bad: global mutable state
|
|
409
|
+
var db *sql.DB
|
|
410
|
+
|
|
411
|
+
func init() {
|
|
412
|
+
db, _ = sql.Open("postgres", os.Getenv("DATABASE_URL"))
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
// Good: dependency injection
|
|
416
|
+
type Server struct {
|
|
417
|
+
db *sql.DB
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
func NewServer(db *sql.DB) *Server {
|
|
421
|
+
return &Server{db: db}
|
|
422
|
+
}
|
|
423
|
+
\`\`\`
|
|
424
|
+
|
|
425
|
+
## Struct design
|
|
426
|
+
|
|
427
|
+
### Functional options pattern
|
|
428
|
+
|
|
429
|
+
\`\`\`go
|
|
430
|
+
type Server struct {
|
|
431
|
+
addr string
|
|
432
|
+
timeout time.Duration
|
|
433
|
+
logger *log.Logger
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
type Option func(*Server)
|
|
437
|
+
|
|
438
|
+
func WithTimeout(d time.Duration) Option {
|
|
439
|
+
return func(s *Server) {
|
|
440
|
+
s.timeout = d
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
func WithLogger(l *log.Logger) Option {
|
|
445
|
+
return func(s *Server) {
|
|
446
|
+
s.logger = l
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
func NewServer(addr string, opts ...Option) *Server {
|
|
451
|
+
s := &Server{
|
|
452
|
+
addr: addr,
|
|
453
|
+
timeout: 30 * time.Second, // default
|
|
454
|
+
logger: log.Default(), // default
|
|
455
|
+
}
|
|
456
|
+
for _, opt := range opts {
|
|
457
|
+
opt(s)
|
|
458
|
+
}
|
|
459
|
+
return s
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
// Usage
|
|
463
|
+
server := NewServer(":8080",
|
|
464
|
+
WithTimeout(60*time.Second),
|
|
465
|
+
WithLogger(customLogger),
|
|
466
|
+
)
|
|
467
|
+
\`\`\`
|
|
468
|
+
|
|
469
|
+
### Embedding for composition
|
|
470
|
+
|
|
471
|
+
\`\`\`go
|
|
472
|
+
type Logger struct {
|
|
473
|
+
prefix string
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
func (l *Logger) Log(msg string) {
|
|
477
|
+
fmt.Printf("[%s] %s\n", l.prefix, msg)
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
type Server struct {
|
|
481
|
+
*Logger // Embedding - Server gets Log method
|
|
482
|
+
addr string
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
func NewServer(addr string) *Server {
|
|
486
|
+
return &Server{
|
|
487
|
+
Logger: &Logger{prefix: "SERVER"},
|
|
488
|
+
addr: addr,
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
// Usage
|
|
493
|
+
s := NewServer(":8080")
|
|
494
|
+
s.Log("Starting...") // Calls embedded Logger.Log
|
|
495
|
+
\`\`\`
|
|
496
|
+
|
|
497
|
+
## Memory and performance
|
|
498
|
+
|
|
499
|
+
### Preallocate slices when size is known
|
|
500
|
+
|
|
501
|
+
\`\`\`go
|
|
502
|
+
// Bad: grows slice multiple times
|
|
503
|
+
func processItems(items []Item) []Result {
|
|
504
|
+
var results []Result
|
|
505
|
+
for _, item := range items {
|
|
506
|
+
results = append(results, process(item))
|
|
507
|
+
}
|
|
508
|
+
return results
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
// Good: single allocation
|
|
512
|
+
func processItems(items []Item) []Result {
|
|
513
|
+
results := make([]Result, 0, len(items))
|
|
514
|
+
for _, item := range items {
|
|
515
|
+
results = append(results, process(item))
|
|
516
|
+
}
|
|
517
|
+
return results
|
|
518
|
+
}
|
|
519
|
+
\`\`\`
|
|
520
|
+
|
|
521
|
+
### Use sync.Pool for frequent allocations
|
|
522
|
+
|
|
523
|
+
\`\`\`go
|
|
524
|
+
var bufferPool = sync.Pool{
|
|
525
|
+
New: func() interface{} {
|
|
526
|
+
return new(bytes.Buffer)
|
|
527
|
+
},
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
func ProcessRequest(data []byte) []byte {
|
|
531
|
+
buf := bufferPool.Get().(*bytes.Buffer)
|
|
532
|
+
defer func() {
|
|
533
|
+
buf.Reset()
|
|
534
|
+
bufferPool.Put(buf)
|
|
535
|
+
}()
|
|
536
|
+
|
|
537
|
+
buf.Write(data)
|
|
538
|
+
// Process...
|
|
539
|
+
return buf.Bytes()
|
|
540
|
+
}
|
|
541
|
+
\`\`\`
|
|
542
|
+
|
|
543
|
+
### Avoid string concatenation in loops
|
|
544
|
+
|
|
545
|
+
\`\`\`go
|
|
546
|
+
// Bad: creates many string allocations
|
|
547
|
+
func join(parts []string) string {
|
|
548
|
+
var result string
|
|
549
|
+
for _, p := range parts {
|
|
550
|
+
result += p + ","
|
|
551
|
+
}
|
|
552
|
+
return result
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
// Good: single allocation with strings.Builder
|
|
556
|
+
func join(parts []string) string {
|
|
557
|
+
var sb strings.Builder
|
|
558
|
+
for i, p := range parts {
|
|
559
|
+
if i > 0 {
|
|
560
|
+
sb.WriteString(",")
|
|
561
|
+
}
|
|
562
|
+
sb.WriteString(p)
|
|
563
|
+
}
|
|
564
|
+
return sb.String()
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
// Best: use standard library
|
|
568
|
+
func join(parts []string) string {
|
|
569
|
+
return strings.Join(parts, ",")
|
|
570
|
+
}
|
|
571
|
+
\`\`\`
|
|
572
|
+
|
|
573
|
+
## Go tooling integration
|
|
574
|
+
|
|
575
|
+
### Essential commands
|
|
576
|
+
|
|
577
|
+
\`\`\`bash
|
|
578
|
+
# Build and run
|
|
579
|
+
go build ./...
|
|
580
|
+
go run ./cmd/myapp
|
|
581
|
+
|
|
582
|
+
# Testing
|
|
583
|
+
go test ./...
|
|
584
|
+
go test -race ./...
|
|
585
|
+
go test -cover ./...
|
|
586
|
+
|
|
587
|
+
# Static analysis
|
|
588
|
+
go vet ./...
|
|
589
|
+
staticcheck ./...
|
|
590
|
+
golangci-lint run
|
|
591
|
+
|
|
592
|
+
# Module management
|
|
593
|
+
go mod tidy
|
|
594
|
+
go mod verify
|
|
595
|
+
|
|
596
|
+
# Formatting
|
|
597
|
+
gofmt -w .
|
|
598
|
+
goimports -w .
|
|
599
|
+
\`\`\`
|
|
600
|
+
|
|
601
|
+
### Recommended linter configuration (.golangci.yml)
|
|
602
|
+
|
|
603
|
+
\`\`\`yaml
|
|
604
|
+
linters:
|
|
605
|
+
enable:
|
|
606
|
+
- errcheck
|
|
607
|
+
- gosimple
|
|
608
|
+
- govet
|
|
609
|
+
- ineffassign
|
|
610
|
+
- staticcheck
|
|
611
|
+
- unused
|
|
612
|
+
- gofmt
|
|
613
|
+
- goimports
|
|
614
|
+
- misspell
|
|
615
|
+
- unconvert
|
|
616
|
+
- unparam
|
|
617
|
+
|
|
618
|
+
linters-settings:
|
|
619
|
+
errcheck:
|
|
620
|
+
check-type-assertions: true
|
|
621
|
+
govet:
|
|
622
|
+
enable:
|
|
623
|
+
- shadow
|
|
624
|
+
|
|
625
|
+
issues:
|
|
626
|
+
exclude-use-default: false
|
|
627
|
+
\`\`\`
|
|
628
|
+
|
|
629
|
+
## Quick reference: Go idioms
|
|
630
|
+
|
|
631
|
+
| Idiom | Description |
|
|
632
|
+
|-------|-------------|
|
|
633
|
+
| Accept interfaces, return structs | Functions accept interface params, return concrete types |
|
|
634
|
+
| Errors are values | Treat errors as first-class values, not exceptions |
|
|
635
|
+
| Don't communicate by sharing memory | Use channels for coordination between goroutines |
|
|
636
|
+
| Make the zero value useful | Types should work without explicit initialization |
|
|
637
|
+
| A little copying is better than a little dependency | Avoid unnecessary external dependencies |
|
|
638
|
+
| Clear is better than clever | Prioritize readability over cleverness |
|
|
639
|
+
| gofmt is no one's favorite but everyone's friend | Always format with gofmt/goimports |
|
|
640
|
+
| Return early | Handle errors first, keep happy path unindented |
|
|
641
|
+
|
|
642
|
+
## Anti-patterns to avoid
|
|
643
|
+
|
|
644
|
+
\`\`\`go
|
|
645
|
+
// Bad: naked returns in long functions
|
|
646
|
+
func process() (result int, err error) {
|
|
647
|
+
// ... 50 lines ...
|
|
648
|
+
return // What is being returned?
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
// Bad: using panic for control flow
|
|
652
|
+
func GetUser(id string) *User {
|
|
653
|
+
user, err := db.Find(id)
|
|
654
|
+
if err != nil {
|
|
655
|
+
panic(err) // Don't do this
|
|
656
|
+
}
|
|
657
|
+
return user
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
// Bad: passing context in struct
|
|
661
|
+
type Request struct {
|
|
662
|
+
ctx context.Context // Context should be first param
|
|
663
|
+
ID string
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
// Good: context as first parameter
|
|
667
|
+
func ProcessRequest(ctx context.Context, id string) error {
|
|
668
|
+
// ...
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
// Bad: mixing value and pointer receivers
|
|
672
|
+
type Counter struct{ n int }
|
|
673
|
+
func (c Counter) Value() int { return c.n } // Value receiver
|
|
674
|
+
func (c *Counter) Increment() { c.n++ } // Pointer receiver
|
|
675
|
+
// Pick one style and be consistent
|
|
676
|
+
\`\`\`
|
|
677
|
+
|
|
678
|
+
**Remember**: Go code should be boring in the best way — predictable, consistent, and easy to understand. When in doubt, keep it simple.
|
|
679
|
+
|
|
680
|
+
### golang-testing
|
|
681
|
+
|
|
682
|
+
Go testing patterns including TDD workflow, table-driven tests, subtests, benchmarks, fuzzing, golden files, HTTP handler testing, mocking with interfaces, and coverage targets. Use when writing new Go functions, adding test coverage, creating benchmarks, or following TDD in a Go project.
|
|
683
|
+
|
|
684
|
+
# Go Testing Patterns
|
|
685
|
+
|
|
686
|
+
Comprehensive Go testing patterns for writing reliable, maintainable tests following TDD methodology.
|
|
687
|
+
|
|
688
|
+
## When to use
|
|
689
|
+
|
|
690
|
+
- Writing new Go functions or methods.
|
|
691
|
+
- Adding test coverage to existing code.
|
|
692
|
+
- Creating benchmarks for performance-critical code.
|
|
693
|
+
- Implementing fuzz tests for input validation.
|
|
694
|
+
- Following a TDD workflow in Go projects.
|
|
695
|
+
|
|
696
|
+
## TDD workflow for Go
|
|
697
|
+
|
|
698
|
+
### The RED-GREEN-REFACTOR cycle
|
|
699
|
+
|
|
700
|
+
\`\`\`
|
|
701
|
+
RED → Write a failing test first
|
|
702
|
+
GREEN → Write minimal code to pass the test
|
|
703
|
+
REFACTOR → Improve code while keeping tests green
|
|
704
|
+
REPEAT → Continue with next requirement
|
|
705
|
+
\`\`\`
|
|
706
|
+
|
|
707
|
+
### Step-by-step TDD in Go
|
|
708
|
+
|
|
709
|
+
\`\`\`go
|
|
710
|
+
// Step 1: Define the interface/signature
|
|
711
|
+
// calculator.go
|
|
712
|
+
package calculator
|
|
713
|
+
|
|
714
|
+
func Add(a, b int) int {
|
|
715
|
+
panic("not implemented") // Placeholder
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
// Step 2: Write failing test (RED)
|
|
719
|
+
// calculator_test.go
|
|
720
|
+
package calculator
|
|
721
|
+
|
|
722
|
+
import "testing"
|
|
723
|
+
|
|
724
|
+
func TestAdd(t *testing.T) {
|
|
725
|
+
got := Add(2, 3)
|
|
726
|
+
want := 5
|
|
727
|
+
if got != want {
|
|
728
|
+
t.Errorf("Add(2, 3) = %d; want %d", got, want)
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
// Step 3: Run test - verify FAIL
|
|
733
|
+
// $ go test
|
|
734
|
+
// --- FAIL: TestAdd (0.00s)
|
|
735
|
+
// panic: not implemented
|
|
736
|
+
|
|
737
|
+
// Step 4: Implement minimal code (GREEN)
|
|
738
|
+
func Add(a, b int) int {
|
|
739
|
+
return a + b
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
// Step 5: Run test - verify PASS
|
|
743
|
+
// $ go test
|
|
744
|
+
// PASS
|
|
745
|
+
|
|
746
|
+
// Step 6: Refactor if needed, verify tests still pass
|
|
747
|
+
\`\`\`
|
|
748
|
+
|
|
749
|
+
## Table-driven tests
|
|
750
|
+
|
|
751
|
+
The standard pattern for Go tests. Enables comprehensive coverage with minimal code.
|
|
752
|
+
|
|
753
|
+
\`\`\`go
|
|
754
|
+
func TestAdd(t *testing.T) {
|
|
755
|
+
tests := []struct {
|
|
756
|
+
name string
|
|
757
|
+
a, b int
|
|
758
|
+
expected int
|
|
759
|
+
}{
|
|
760
|
+
{"positive numbers", 2, 3, 5},
|
|
761
|
+
{"negative numbers", -1, -2, -3},
|
|
762
|
+
{"zero values", 0, 0, 0},
|
|
763
|
+
{"mixed signs", -1, 1, 0},
|
|
764
|
+
{"large numbers", 1000000, 2000000, 3000000},
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
for _, tt := range tests {
|
|
768
|
+
t.Run(tt.name, func(t *testing.T) {
|
|
769
|
+
got := Add(tt.a, tt.b)
|
|
770
|
+
if got != tt.expected {
|
|
771
|
+
t.Errorf("Add(%d, %d) = %d; want %d",
|
|
772
|
+
tt.a, tt.b, got, tt.expected)
|
|
773
|
+
}
|
|
774
|
+
})
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
\`\`\`
|
|
778
|
+
|
|
779
|
+
### Table-driven tests with error cases
|
|
780
|
+
|
|
781
|
+
\`\`\`go
|
|
782
|
+
func TestParseConfig(t *testing.T) {
|
|
783
|
+
tests := []struct {
|
|
784
|
+
name string
|
|
785
|
+
input string
|
|
786
|
+
want *Config
|
|
787
|
+
wantErr bool
|
|
788
|
+
}{
|
|
789
|
+
{
|
|
790
|
+
name: "valid config",
|
|
791
|
+
input: \`{"host": "localhost", "port": 8080}\`,
|
|
792
|
+
want: &Config{Host: "localhost", Port: 8080},
|
|
793
|
+
},
|
|
794
|
+
{
|
|
795
|
+
name: "invalid JSON",
|
|
796
|
+
input: \`{invalid}\`,
|
|
797
|
+
wantErr: true,
|
|
798
|
+
},
|
|
799
|
+
{
|
|
800
|
+
name: "empty input",
|
|
801
|
+
input: "",
|
|
802
|
+
wantErr: true,
|
|
803
|
+
},
|
|
804
|
+
{
|
|
805
|
+
name: "minimal config",
|
|
806
|
+
input: \`{}\`,
|
|
807
|
+
want: &Config{}, // Zero value config
|
|
808
|
+
},
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
for _, tt := range tests {
|
|
812
|
+
t.Run(tt.name, func(t *testing.T) {
|
|
813
|
+
got, err := ParseConfig(tt.input)
|
|
814
|
+
|
|
815
|
+
if tt.wantErr {
|
|
816
|
+
if err == nil {
|
|
817
|
+
t.Error("expected error, got nil")
|
|
818
|
+
}
|
|
819
|
+
return
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
if err != nil {
|
|
823
|
+
t.Fatalf("unexpected error: %v", err)
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
if !reflect.DeepEqual(got, tt.want) {
|
|
827
|
+
t.Errorf("got %+v; want %+v", got, tt.want)
|
|
828
|
+
}
|
|
829
|
+
})
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
\`\`\`
|
|
833
|
+
|
|
834
|
+
## Subtests and sub-benchmarks
|
|
835
|
+
|
|
836
|
+
### Organizing related tests
|
|
837
|
+
|
|
838
|
+
\`\`\`go
|
|
839
|
+
func TestUser(t *testing.T) {
|
|
840
|
+
// Setup shared by all subtests
|
|
841
|
+
db := setupTestDB(t)
|
|
842
|
+
|
|
843
|
+
t.Run("Create", func(t *testing.T) {
|
|
844
|
+
user := &User{Name: "Alice"}
|
|
845
|
+
err := db.CreateUser(user)
|
|
846
|
+
if err != nil {
|
|
847
|
+
t.Fatalf("CreateUser failed: %v", err)
|
|
848
|
+
}
|
|
849
|
+
if user.ID == "" {
|
|
850
|
+
t.Error("expected user ID to be set")
|
|
851
|
+
}
|
|
852
|
+
})
|
|
853
|
+
|
|
854
|
+
t.Run("Get", func(t *testing.T) {
|
|
855
|
+
user, err := db.GetUser("alice-id")
|
|
856
|
+
if err != nil {
|
|
857
|
+
t.Fatalf("GetUser failed: %v", err)
|
|
858
|
+
}
|
|
859
|
+
if user.Name != "Alice" {
|
|
860
|
+
t.Errorf("got name %q; want %q", user.Name, "Alice")
|
|
861
|
+
}
|
|
862
|
+
})
|
|
863
|
+
|
|
864
|
+
t.Run("Update", func(t *testing.T) {
|
|
865
|
+
// ...
|
|
866
|
+
})
|
|
867
|
+
|
|
868
|
+
t.Run("Delete", func(t *testing.T) {
|
|
869
|
+
// ...
|
|
870
|
+
})
|
|
871
|
+
}
|
|
872
|
+
\`\`\`
|
|
873
|
+
|
|
874
|
+
### Parallel subtests
|
|
875
|
+
|
|
876
|
+
\`\`\`go
|
|
877
|
+
func TestParallel(t *testing.T) {
|
|
878
|
+
tests := []struct {
|
|
879
|
+
name string
|
|
880
|
+
input string
|
|
881
|
+
}{
|
|
882
|
+
{"case1", "input1"},
|
|
883
|
+
{"case2", "input2"},
|
|
884
|
+
{"case3", "input3"},
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
for _, tt := range tests {
|
|
888
|
+
tt := tt // Capture range variable
|
|
889
|
+
t.Run(tt.name, func(t *testing.T) {
|
|
890
|
+
t.Parallel() // Run subtests in parallel
|
|
891
|
+
result := Process(tt.input)
|
|
892
|
+
// assertions...
|
|
893
|
+
_ = result
|
|
894
|
+
})
|
|
895
|
+
}
|
|
896
|
+
}
|
|
897
|
+
\`\`\`
|
|
898
|
+
|
|
899
|
+
## Test helpers
|
|
900
|
+
|
|
901
|
+
### Helper functions
|
|
902
|
+
|
|
903
|
+
\`\`\`go
|
|
904
|
+
func setupTestDB(t *testing.T) *sql.DB {
|
|
905
|
+
t.Helper() // Marks this as a helper function
|
|
906
|
+
|
|
907
|
+
db, err := sql.Open("sqlite3", ":memory:")
|
|
908
|
+
if err != nil {
|
|
909
|
+
t.Fatalf("failed to open database: %v", err)
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
// Cleanup when test finishes
|
|
913
|
+
t.Cleanup(func() {
|
|
914
|
+
db.Close()
|
|
915
|
+
})
|
|
916
|
+
|
|
917
|
+
// Run migrations
|
|
918
|
+
if _, err := db.Exec(schema); err != nil {
|
|
919
|
+
t.Fatalf("failed to create schema: %v", err)
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
return db
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
func assertNoError(t *testing.T, err error) {
|
|
926
|
+
t.Helper()
|
|
927
|
+
if err != nil {
|
|
928
|
+
t.Fatalf("unexpected error: %v", err)
|
|
929
|
+
}
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
func assertEqual[T comparable](t *testing.T, got, want T) {
|
|
933
|
+
t.Helper()
|
|
934
|
+
if got != want {
|
|
935
|
+
t.Errorf("got %v; want %v", got, want)
|
|
936
|
+
}
|
|
937
|
+
}
|
|
938
|
+
\`\`\`
|
|
939
|
+
|
|
940
|
+
### Temporary files and directories
|
|
941
|
+
|
|
942
|
+
\`\`\`go
|
|
943
|
+
func TestFileProcessing(t *testing.T) {
|
|
944
|
+
// Create temp directory - automatically cleaned up
|
|
945
|
+
tmpDir := t.TempDir()
|
|
946
|
+
|
|
947
|
+
// Create test file
|
|
948
|
+
testFile := filepath.Join(tmpDir, "test.txt")
|
|
949
|
+
err := os.WriteFile(testFile, []byte("test content"), 0644)
|
|
950
|
+
if err != nil {
|
|
951
|
+
t.Fatalf("failed to create test file: %v", err)
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
// Run test
|
|
955
|
+
result, err := ProcessFile(testFile)
|
|
956
|
+
if err != nil {
|
|
957
|
+
t.Fatalf("ProcessFile failed: %v", err)
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
// Assert...
|
|
961
|
+
_ = result
|
|
962
|
+
}
|
|
963
|
+
\`\`\`
|
|
964
|
+
|
|
965
|
+
## Golden files
|
|
966
|
+
|
|
967
|
+
Testing against expected output files stored in \`testdata/\`.
|
|
968
|
+
|
|
969
|
+
\`\`\`go
|
|
970
|
+
var update = flag.Bool("update", false, "update golden files")
|
|
971
|
+
|
|
972
|
+
func TestRender(t *testing.T) {
|
|
973
|
+
tests := []struct {
|
|
974
|
+
name string
|
|
975
|
+
input Template
|
|
976
|
+
}{
|
|
977
|
+
{"simple", Template{Name: "test"}},
|
|
978
|
+
{"complex", Template{Name: "test", Items: []string{"a", "b"}}},
|
|
979
|
+
}
|
|
980
|
+
|
|
981
|
+
for _, tt := range tests {
|
|
982
|
+
t.Run(tt.name, func(t *testing.T) {
|
|
983
|
+
got := Render(tt.input)
|
|
984
|
+
|
|
985
|
+
golden := filepath.Join("testdata", tt.name+".golden")
|
|
986
|
+
|
|
987
|
+
if *update {
|
|
988
|
+
// Update golden file: go test -update
|
|
989
|
+
err := os.WriteFile(golden, got, 0644)
|
|
990
|
+
if err != nil {
|
|
991
|
+
t.Fatalf("failed to update golden file: %v", err)
|
|
992
|
+
}
|
|
993
|
+
}
|
|
994
|
+
|
|
995
|
+
want, err := os.ReadFile(golden)
|
|
996
|
+
if err != nil {
|
|
997
|
+
t.Fatalf("failed to read golden file: %v", err)
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
if !bytes.Equal(got, want) {
|
|
1001
|
+
t.Errorf("output mismatch:\ngot:\n%s\nwant:\n%s", got, want)
|
|
1002
|
+
}
|
|
1003
|
+
})
|
|
1004
|
+
}
|
|
1005
|
+
}
|
|
1006
|
+
\`\`\`
|
|
1007
|
+
|
|
1008
|
+
## Mocking with interfaces
|
|
1009
|
+
|
|
1010
|
+
### Interface-based mocking
|
|
1011
|
+
|
|
1012
|
+
\`\`\`go
|
|
1013
|
+
// Define interface for dependencies
|
|
1014
|
+
type UserRepository interface {
|
|
1015
|
+
GetUser(id string) (*User, error)
|
|
1016
|
+
SaveUser(user *User) error
|
|
1017
|
+
}
|
|
1018
|
+
|
|
1019
|
+
// Production implementation
|
|
1020
|
+
type PostgresUserRepository struct {
|
|
1021
|
+
db *sql.DB
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
func (r *PostgresUserRepository) GetUser(id string) (*User, error) {
|
|
1025
|
+
// Real database query
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1028
|
+
// Mock implementation for tests
|
|
1029
|
+
type MockUserRepository struct {
|
|
1030
|
+
GetUserFunc func(id string) (*User, error)
|
|
1031
|
+
SaveUserFunc func(user *User) error
|
|
1032
|
+
}
|
|
1033
|
+
|
|
1034
|
+
func (m *MockUserRepository) GetUser(id string) (*User, error) {
|
|
1035
|
+
return m.GetUserFunc(id)
|
|
1036
|
+
}
|
|
1037
|
+
|
|
1038
|
+
func (m *MockUserRepository) SaveUser(user *User) error {
|
|
1039
|
+
return m.SaveUserFunc(user)
|
|
1040
|
+
}
|
|
1041
|
+
|
|
1042
|
+
// Test using mock
|
|
1043
|
+
func TestUserService(t *testing.T) {
|
|
1044
|
+
mock := &MockUserRepository{
|
|
1045
|
+
GetUserFunc: func(id string) (*User, error) {
|
|
1046
|
+
if id == "123" {
|
|
1047
|
+
return &User{ID: "123", Name: "Alice"}, nil
|
|
1048
|
+
}
|
|
1049
|
+
return nil, ErrNotFound
|
|
1050
|
+
},
|
|
1051
|
+
}
|
|
1052
|
+
|
|
1053
|
+
service := NewUserService(mock)
|
|
1054
|
+
|
|
1055
|
+
user, err := service.GetUserProfile("123")
|
|
1056
|
+
if err != nil {
|
|
1057
|
+
t.Fatalf("unexpected error: %v", err)
|
|
1058
|
+
}
|
|
1059
|
+
if user.Name != "Alice" {
|
|
1060
|
+
t.Errorf("got name %q; want %q", user.Name, "Alice")
|
|
1061
|
+
}
|
|
1062
|
+
}
|
|
1063
|
+
\`\`\`
|
|
1064
|
+
|
|
1065
|
+
## Benchmarks
|
|
1066
|
+
|
|
1067
|
+
### Basic benchmarks
|
|
1068
|
+
|
|
1069
|
+
\`\`\`go
|
|
1070
|
+
func BenchmarkProcess(b *testing.B) {
|
|
1071
|
+
data := generateTestData(1000)
|
|
1072
|
+
b.ResetTimer() // Don't count setup time
|
|
1073
|
+
|
|
1074
|
+
for i := 0; i < b.N; i++ {
|
|
1075
|
+
Process(data)
|
|
1076
|
+
}
|
|
1077
|
+
}
|
|
1078
|
+
|
|
1079
|
+
// Run: go test -bench=BenchmarkProcess -benchmem
|
|
1080
|
+
// Output: BenchmarkProcess-8 10000 105234 ns/op 4096 B/op 10 allocs/op
|
|
1081
|
+
\`\`\`
|
|
1082
|
+
|
|
1083
|
+
### Benchmark with different sizes
|
|
1084
|
+
|
|
1085
|
+
\`\`\`go
|
|
1086
|
+
func BenchmarkSort(b *testing.B) {
|
|
1087
|
+
sizes := []int{100, 1000, 10000, 100000}
|
|
1088
|
+
|
|
1089
|
+
for _, size := range sizes {
|
|
1090
|
+
b.Run(fmt.Sprintf("size=%d", size), func(b *testing.B) {
|
|
1091
|
+
data := generateRandomSlice(size)
|
|
1092
|
+
b.ResetTimer()
|
|
1093
|
+
|
|
1094
|
+
for i := 0; i < b.N; i++ {
|
|
1095
|
+
// Make a copy to avoid sorting already sorted data
|
|
1096
|
+
tmp := make([]int, len(data))
|
|
1097
|
+
copy(tmp, data)
|
|
1098
|
+
sort.Ints(tmp)
|
|
1099
|
+
}
|
|
1100
|
+
})
|
|
1101
|
+
}
|
|
1102
|
+
}
|
|
1103
|
+
\`\`\`
|
|
1104
|
+
|
|
1105
|
+
### Memory allocation benchmarks
|
|
1106
|
+
|
|
1107
|
+
\`\`\`go
|
|
1108
|
+
func BenchmarkStringConcat(b *testing.B) {
|
|
1109
|
+
parts := []string{"hello", "world", "foo", "bar", "baz"}
|
|
1110
|
+
|
|
1111
|
+
b.Run("plus", func(b *testing.B) {
|
|
1112
|
+
for i := 0; i < b.N; i++ {
|
|
1113
|
+
var s string
|
|
1114
|
+
for _, p := range parts {
|
|
1115
|
+
s += p
|
|
1116
|
+
}
|
|
1117
|
+
_ = s
|
|
1118
|
+
}
|
|
1119
|
+
})
|
|
1120
|
+
|
|
1121
|
+
b.Run("builder", func(b *testing.B) {
|
|
1122
|
+
for i := 0; i < b.N; i++ {
|
|
1123
|
+
var sb strings.Builder
|
|
1124
|
+
for _, p := range parts {
|
|
1125
|
+
sb.WriteString(p)
|
|
1126
|
+
}
|
|
1127
|
+
_ = sb.String()
|
|
1128
|
+
}
|
|
1129
|
+
})
|
|
1130
|
+
|
|
1131
|
+
b.Run("join", func(b *testing.B) {
|
|
1132
|
+
for i := 0; i < b.N; i++ {
|
|
1133
|
+
_ = strings.Join(parts, "")
|
|
1134
|
+
}
|
|
1135
|
+
})
|
|
1136
|
+
}
|
|
1137
|
+
\`\`\`
|
|
1138
|
+
|
|
1139
|
+
## Fuzzing (Go 1.18+)
|
|
1140
|
+
|
|
1141
|
+
### Basic fuzz test
|
|
1142
|
+
|
|
1143
|
+
\`\`\`go
|
|
1144
|
+
func FuzzParseJSON(f *testing.F) {
|
|
1145
|
+
// Add seed corpus
|
|
1146
|
+
f.Add(\`{"name": "test"}\`)
|
|
1147
|
+
f.Add(\`{"count": 123}\`)
|
|
1148
|
+
f.Add(\`[]\`)
|
|
1149
|
+
f.Add(\`""\`)
|
|
1150
|
+
|
|
1151
|
+
f.Fuzz(func(t *testing.T, input string) {
|
|
1152
|
+
var result map[string]interface{}
|
|
1153
|
+
err := json.Unmarshal([]byte(input), &result)
|
|
1154
|
+
|
|
1155
|
+
if err != nil {
|
|
1156
|
+
// Invalid JSON is expected for random input
|
|
1157
|
+
return
|
|
1158
|
+
}
|
|
1159
|
+
|
|
1160
|
+
// If parsing succeeded, re-encoding should work
|
|
1161
|
+
_, err = json.Marshal(result)
|
|
1162
|
+
if err != nil {
|
|
1163
|
+
t.Errorf("Marshal failed after successful Unmarshal: %v", err)
|
|
1164
|
+
}
|
|
1165
|
+
})
|
|
1166
|
+
}
|
|
1167
|
+
|
|
1168
|
+
// Run: go test -fuzz=FuzzParseJSON -fuzztime=30s
|
|
1169
|
+
\`\`\`
|
|
1170
|
+
|
|
1171
|
+
### Fuzz test with multiple inputs
|
|
1172
|
+
|
|
1173
|
+
\`\`\`go
|
|
1174
|
+
func FuzzCompare(f *testing.F) {
|
|
1175
|
+
f.Add("hello", "world")
|
|
1176
|
+
f.Add("", "")
|
|
1177
|
+
f.Add("abc", "abc")
|
|
1178
|
+
|
|
1179
|
+
f.Fuzz(func(t *testing.T, a, b string) {
|
|
1180
|
+
result := Compare(a, b)
|
|
1181
|
+
|
|
1182
|
+
// Property: Compare(a, a) should always equal 0
|
|
1183
|
+
if a == b && result != 0 {
|
|
1184
|
+
t.Errorf("Compare(%q, %q) = %d; want 0", a, b, result)
|
|
1185
|
+
}
|
|
1186
|
+
|
|
1187
|
+
// Property: Compare(a, b) and Compare(b, a) should have opposite signs
|
|
1188
|
+
reverse := Compare(b, a)
|
|
1189
|
+
if (result > 0 && reverse >= 0) || (result < 0 && reverse <= 0) {
|
|
1190
|
+
if result != 0 || reverse != 0 {
|
|
1191
|
+
t.Errorf("Compare(%q, %q) = %d, Compare(%q, %q) = %d; inconsistent",
|
|
1192
|
+
a, b, result, b, a, reverse)
|
|
1193
|
+
}
|
|
1194
|
+
}
|
|
1195
|
+
})
|
|
1196
|
+
}
|
|
1197
|
+
\`\`\`
|
|
1198
|
+
|
|
1199
|
+
## Test coverage
|
|
1200
|
+
|
|
1201
|
+
### Running coverage
|
|
1202
|
+
|
|
1203
|
+
\`\`\`bash
|
|
1204
|
+
# Basic coverage
|
|
1205
|
+
go test -cover ./...
|
|
1206
|
+
|
|
1207
|
+
# Generate coverage profile
|
|
1208
|
+
go test -coverprofile=coverage.out ./...
|
|
1209
|
+
|
|
1210
|
+
# View coverage in browser
|
|
1211
|
+
go tool cover -html=coverage.out
|
|
1212
|
+
|
|
1213
|
+
# View coverage by function
|
|
1214
|
+
go tool cover -func=coverage.out
|
|
1215
|
+
|
|
1216
|
+
# Coverage with race detection
|
|
1217
|
+
go test -race -coverprofile=coverage.out ./...
|
|
1218
|
+
\`\`\`
|
|
1219
|
+
|
|
1220
|
+
### Coverage targets
|
|
1221
|
+
|
|
1222
|
+
| Code Type | Target |
|
|
1223
|
+
|-----------|--------|
|
|
1224
|
+
| Critical business logic | 100% |
|
|
1225
|
+
| Public APIs | 90%+ |
|
|
1226
|
+
| General code | 80%+ |
|
|
1227
|
+
| Generated code | Exclude |
|
|
1228
|
+
|
|
1229
|
+
### Excluding generated code from coverage
|
|
1230
|
+
|
|
1231
|
+
\`\`\`go
|
|
1232
|
+
//go:generate mockgen -source=interface.go -destination=mock_interface.go
|
|
1233
|
+
|
|
1234
|
+
// In coverage profile, exclude with build tags:
|
|
1235
|
+
// go test -cover -tags=!generate ./...
|
|
1236
|
+
\`\`\`
|
|
1237
|
+
|
|
1238
|
+
## HTTP handler testing
|
|
1239
|
+
|
|
1240
|
+
\`\`\`go
|
|
1241
|
+
func TestHealthHandler(t *testing.T) {
|
|
1242
|
+
// Create request
|
|
1243
|
+
req := httptest.NewRequest(http.MethodGet, "/health", nil)
|
|
1244
|
+
w := httptest.NewRecorder()
|
|
1245
|
+
|
|
1246
|
+
// Call handler
|
|
1247
|
+
HealthHandler(w, req)
|
|
1248
|
+
|
|
1249
|
+
// Check response
|
|
1250
|
+
resp := w.Result()
|
|
1251
|
+
defer resp.Body.Close()
|
|
1252
|
+
|
|
1253
|
+
if resp.StatusCode != http.StatusOK {
|
|
1254
|
+
t.Errorf("got status %d; want %d", resp.StatusCode, http.StatusOK)
|
|
1255
|
+
}
|
|
1256
|
+
|
|
1257
|
+
body, _ := io.ReadAll(resp.Body)
|
|
1258
|
+
if string(body) != "OK" {
|
|
1259
|
+
t.Errorf("got body %q; want %q", body, "OK")
|
|
1260
|
+
}
|
|
1261
|
+
}
|
|
1262
|
+
|
|
1263
|
+
func TestAPIHandler(t *testing.T) {
|
|
1264
|
+
tests := []struct {
|
|
1265
|
+
name string
|
|
1266
|
+
method string
|
|
1267
|
+
path string
|
|
1268
|
+
body string
|
|
1269
|
+
wantStatus int
|
|
1270
|
+
wantBody string
|
|
1271
|
+
}{
|
|
1272
|
+
{
|
|
1273
|
+
name: "get user",
|
|
1274
|
+
method: http.MethodGet,
|
|
1275
|
+
path: "/users/123",
|
|
1276
|
+
wantStatus: http.StatusOK,
|
|
1277
|
+
wantBody: \`{"id":"123","name":"Alice"}\`,
|
|
1278
|
+
},
|
|
1279
|
+
{
|
|
1280
|
+
name: "not found",
|
|
1281
|
+
method: http.MethodGet,
|
|
1282
|
+
path: "/users/999",
|
|
1283
|
+
wantStatus: http.StatusNotFound,
|
|
1284
|
+
},
|
|
1285
|
+
{
|
|
1286
|
+
name: "create user",
|
|
1287
|
+
method: http.MethodPost,
|
|
1288
|
+
path: "/users",
|
|
1289
|
+
body: \`{"name":"Bob"}\`,
|
|
1290
|
+
wantStatus: http.StatusCreated,
|
|
1291
|
+
},
|
|
1292
|
+
}
|
|
1293
|
+
|
|
1294
|
+
handler := NewAPIHandler()
|
|
1295
|
+
|
|
1296
|
+
for _, tt := range tests {
|
|
1297
|
+
t.Run(tt.name, func(t *testing.T) {
|
|
1298
|
+
var body io.Reader
|
|
1299
|
+
if tt.body != "" {
|
|
1300
|
+
body = strings.NewReader(tt.body)
|
|
1301
|
+
}
|
|
1302
|
+
|
|
1303
|
+
req := httptest.NewRequest(tt.method, tt.path, body)
|
|
1304
|
+
req.Header.Set("Content-Type", "application/json")
|
|
1305
|
+
w := httptest.NewRecorder()
|
|
1306
|
+
|
|
1307
|
+
handler.ServeHTTP(w, req)
|
|
1308
|
+
|
|
1309
|
+
if w.Code != tt.wantStatus {
|
|
1310
|
+
t.Errorf("got status %d; want %d", w.Code, tt.wantStatus)
|
|
1311
|
+
}
|
|
1312
|
+
|
|
1313
|
+
if tt.wantBody != "" && w.Body.String() != tt.wantBody {
|
|
1314
|
+
t.Errorf("got body %q; want %q", w.Body.String(), tt.wantBody)
|
|
1315
|
+
}
|
|
1316
|
+
})
|
|
1317
|
+
}
|
|
1318
|
+
}
|
|
1319
|
+
\`\`\`
|
|
1320
|
+
|
|
1321
|
+
## Testing commands
|
|
1322
|
+
|
|
1323
|
+
\`\`\`bash
|
|
1324
|
+
# Run all tests
|
|
1325
|
+
go test ./...
|
|
1326
|
+
|
|
1327
|
+
# Run tests with verbose output
|
|
1328
|
+
go test -v ./...
|
|
1329
|
+
|
|
1330
|
+
# Run specific test
|
|
1331
|
+
go test -run TestAdd ./...
|
|
1332
|
+
|
|
1333
|
+
# Run tests matching pattern
|
|
1334
|
+
go test -run "TestUser/Create" ./...
|
|
1335
|
+
|
|
1336
|
+
# Run tests with race detector
|
|
1337
|
+
go test -race ./...
|
|
1338
|
+
|
|
1339
|
+
# Run tests with coverage
|
|
1340
|
+
go test -cover -coverprofile=coverage.out ./...
|
|
1341
|
+
|
|
1342
|
+
# Run short tests only
|
|
1343
|
+
go test -short ./...
|
|
1344
|
+
|
|
1345
|
+
# Run tests with timeout
|
|
1346
|
+
go test -timeout 30s ./...
|
|
1347
|
+
|
|
1348
|
+
# Run benchmarks
|
|
1349
|
+
go test -bench=. -benchmem ./...
|
|
1350
|
+
|
|
1351
|
+
# Run fuzzing
|
|
1352
|
+
go test -fuzz=FuzzParse -fuzztime=30s ./...
|
|
1353
|
+
|
|
1354
|
+
# Count test runs (for flaky test detection)
|
|
1355
|
+
go test -count=10 ./...
|
|
1356
|
+
\`\`\`
|
|
1357
|
+
|
|
1358
|
+
## Best practices
|
|
1359
|
+
|
|
1360
|
+
**DO:**
|
|
1361
|
+
- Write tests FIRST (TDD).
|
|
1362
|
+
- Use table-driven tests for comprehensive coverage.
|
|
1363
|
+
- Test behavior, not implementation.
|
|
1364
|
+
- Use \`t.Helper()\` in helper functions.
|
|
1365
|
+
- Use \`t.Parallel()\` for independent tests.
|
|
1366
|
+
- Clean up resources with \`t.Cleanup()\`.
|
|
1367
|
+
- Use meaningful test names that describe the scenario.
|
|
1368
|
+
|
|
1369
|
+
**DON'T:**
|
|
1370
|
+
- Test private functions directly (test through the public API).
|
|
1371
|
+
- Use \`time.Sleep()\` in tests (use channels or conditions).
|
|
1372
|
+
- Ignore flaky tests (fix or remove them).
|
|
1373
|
+
- Mock everything (prefer integration tests when possible).
|
|
1374
|
+
- Skip error path testing.
|
|
1375
|
+
|
|
1376
|
+
## Integration with CI/CD
|
|
1377
|
+
|
|
1378
|
+
\`\`\`yaml
|
|
1379
|
+
# GitHub Actions example
|
|
1380
|
+
test:
|
|
1381
|
+
runs-on: ubuntu-latest
|
|
1382
|
+
steps:
|
|
1383
|
+
- uses: actions/checkout@v4
|
|
1384
|
+
- uses: actions/setup-go@v5
|
|
1385
|
+
with:
|
|
1386
|
+
go-version: '1.22'
|
|
1387
|
+
|
|
1388
|
+
- name: Run tests
|
|
1389
|
+
run: go test -race -coverprofile=coverage.out ./...
|
|
1390
|
+
|
|
1391
|
+
- name: Check coverage
|
|
1392
|
+
run: |
|
|
1393
|
+
go tool cover -func=coverage.out | grep total | awk '{print $3}' | \
|
|
1394
|
+
awk -F'%' '{if ($1 < 80) exit 1}'
|
|
1395
|
+
\`\`\`
|
|
1396
|
+
|
|
1397
|
+
**Remember**: Tests are documentation. They show how your code is meant to be used. Write them clearly and keep them up to date.
|