agentic-team-templates 0.11.0 → 0.12.1
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 +18 -14
- package/package.json +1 -1
- package/src/index.js +4 -0
- package/src/index.test.js +1 -0
- package/templates/golang-expert/.cursorrules/concurrency.md +290 -0
- package/templates/golang-expert/.cursorrules/error-handling.md +199 -0
- package/templates/golang-expert/.cursorrules/interfaces-and-types.md +255 -0
- package/templates/golang-expert/.cursorrules/overview.md +139 -0
- package/templates/golang-expert/.cursorrules/performance.md +234 -0
- package/templates/golang-expert/.cursorrules/production-patterns.md +320 -0
- package/templates/golang-expert/.cursorrules/stdlib-and-tooling.md +276 -0
- package/templates/golang-expert/.cursorrules/testing.md +326 -0
- package/templates/golang-expert/CLAUDE.md +361 -0
|
@@ -0,0 +1,326 @@
|
|
|
1
|
+
# Go Testing
|
|
2
|
+
|
|
3
|
+
Go has testing built into the language and toolchain. No excuses. Tests are written in `_test.go` files, run with `go test`, and that's it.
|
|
4
|
+
|
|
5
|
+
## Fundamentals
|
|
6
|
+
|
|
7
|
+
### Table-Driven Tests
|
|
8
|
+
|
|
9
|
+
The standard pattern. Use it for any function with multiple input/output cases.
|
|
10
|
+
|
|
11
|
+
```go
|
|
12
|
+
func TestParseAge(t *testing.T) {
|
|
13
|
+
tests := []struct {
|
|
14
|
+
name string
|
|
15
|
+
input string
|
|
16
|
+
want int
|
|
17
|
+
wantErr bool
|
|
18
|
+
}{
|
|
19
|
+
{name: "valid age", input: "25", want: 25},
|
|
20
|
+
{name: "zero", input: "0", want: 0},
|
|
21
|
+
{name: "negative", input: "-1", wantErr: true},
|
|
22
|
+
{name: "non-numeric", input: "abc", wantErr: true},
|
|
23
|
+
{name: "empty string", input: "", wantErr: true},
|
|
24
|
+
{name: "max int boundary", input: "150", wantErr: true},
|
|
25
|
+
{name: "leading zeros", input: "025", want: 25},
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
for _, tt := range tests {
|
|
29
|
+
t.Run(tt.name, func(t *testing.T) {
|
|
30
|
+
got, err := ParseAge(tt.input)
|
|
31
|
+
if tt.wantErr {
|
|
32
|
+
if err == nil {
|
|
33
|
+
t.Errorf("ParseAge(%q) expected error, got %d", tt.input, got)
|
|
34
|
+
}
|
|
35
|
+
return
|
|
36
|
+
}
|
|
37
|
+
if err != nil {
|
|
38
|
+
t.Fatalf("ParseAge(%q) unexpected error: %v", tt.input, err)
|
|
39
|
+
}
|
|
40
|
+
if got != tt.want {
|
|
41
|
+
t.Errorf("ParseAge(%q) = %d, want %d", tt.input, got, tt.want)
|
|
42
|
+
}
|
|
43
|
+
})
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
### Test Naming
|
|
49
|
+
|
|
50
|
+
```go
|
|
51
|
+
// Test function: Test<FunctionName> or Test<Type>_<Method>
|
|
52
|
+
func TestUserService_Create(t *testing.T) { ... }
|
|
53
|
+
|
|
54
|
+
// Subtests: describe the scenario, not the expected result
|
|
55
|
+
t.Run("duplicate email returns ErrAlreadyExists", func(t *testing.T) { ... })
|
|
56
|
+
t.Run("empty name returns validation error", func(t *testing.T) { ... })
|
|
57
|
+
|
|
58
|
+
// Not:
|
|
59
|
+
t.Run("test1", func(t *testing.T) { ... })
|
|
60
|
+
t.Run("success", func(t *testing.T) { ... })
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
### Test Helpers
|
|
64
|
+
|
|
65
|
+
```go
|
|
66
|
+
// Use t.Helper() to mark helper functions
|
|
67
|
+
// so failure messages point to the caller, not the helper
|
|
68
|
+
func assertNoError(t *testing.T, err error) {
|
|
69
|
+
t.Helper()
|
|
70
|
+
if err != nil {
|
|
71
|
+
t.Fatalf("unexpected error: %v", err)
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Return cleanup functions for setup helpers
|
|
76
|
+
func setupTestDB(t *testing.T) *sql.DB {
|
|
77
|
+
t.Helper()
|
|
78
|
+
db, err := sql.Open("sqlite", ":memory:")
|
|
79
|
+
if err != nil {
|
|
80
|
+
t.Fatalf("opening test db: %v", err)
|
|
81
|
+
}
|
|
82
|
+
t.Cleanup(func() { db.Close() })
|
|
83
|
+
return db
|
|
84
|
+
}
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
### Golden Files
|
|
88
|
+
|
|
89
|
+
```go
|
|
90
|
+
// Use testdata/ directory for fixture files
|
|
91
|
+
func TestRender(t *testing.T) {
|
|
92
|
+
input := readTestdata(t, "input.html")
|
|
93
|
+
want := readTestdata(t, "expected_output.html")
|
|
94
|
+
|
|
95
|
+
got, err := Render(input)
|
|
96
|
+
if err != nil {
|
|
97
|
+
t.Fatalf("Render: %v", err)
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if diff := cmp.Diff(want, got); diff != "" {
|
|
101
|
+
t.Errorf("Render mismatch (-want +got):\n%s", diff)
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
func readTestdata(t *testing.T, name string) string {
|
|
106
|
+
t.Helper()
|
|
107
|
+
data, err := os.ReadFile(filepath.Join("testdata", name))
|
|
108
|
+
if err != nil {
|
|
109
|
+
t.Fatalf("reading testdata/%s: %v", name, err)
|
|
110
|
+
}
|
|
111
|
+
return string(data)
|
|
112
|
+
}
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
## Testing Patterns
|
|
116
|
+
|
|
117
|
+
### Dependency Injection via Interfaces
|
|
118
|
+
|
|
119
|
+
```go
|
|
120
|
+
// Define the interface at the consumer
|
|
121
|
+
type UserStore interface {
|
|
122
|
+
GetByID(ctx context.Context, id string) (*User, error)
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Production implementation
|
|
126
|
+
type PostgresUserStore struct { db *sql.DB }
|
|
127
|
+
|
|
128
|
+
// Test implementation — simple and explicit
|
|
129
|
+
type fakeUserStore struct {
|
|
130
|
+
users map[string]*User
|
|
131
|
+
err error
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
func (f *fakeUserStore) GetByID(_ context.Context, id string) (*User, error) {
|
|
135
|
+
if f.err != nil {
|
|
136
|
+
return nil, f.err
|
|
137
|
+
}
|
|
138
|
+
u, ok := f.users[id]
|
|
139
|
+
if !ok {
|
|
140
|
+
return nil, ErrNotFound
|
|
141
|
+
}
|
|
142
|
+
return u, nil
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
func TestHandler_GetUser(t *testing.T) {
|
|
146
|
+
store := &fakeUserStore{
|
|
147
|
+
users: map[string]*User{
|
|
148
|
+
"123": {ID: "123", Name: "Alice"},
|
|
149
|
+
},
|
|
150
|
+
}
|
|
151
|
+
h := NewHandler(store)
|
|
152
|
+
|
|
153
|
+
req := httptest.NewRequest("GET", "/users/123", nil)
|
|
154
|
+
rec := httptest.NewRecorder()
|
|
155
|
+
h.ServeHTTP(rec, req)
|
|
156
|
+
|
|
157
|
+
if rec.Code != http.StatusOK {
|
|
158
|
+
t.Errorf("status = %d, want %d", rec.Code, http.StatusOK)
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
### HTTP Handler Tests
|
|
164
|
+
|
|
165
|
+
```go
|
|
166
|
+
func TestHealthEndpoint(t *testing.T) {
|
|
167
|
+
srv := NewServer(Config{})
|
|
168
|
+
|
|
169
|
+
req := httptest.NewRequest("GET", "/healthz", nil)
|
|
170
|
+
rec := httptest.NewRecorder()
|
|
171
|
+
|
|
172
|
+
srv.ServeHTTP(rec, req)
|
|
173
|
+
|
|
174
|
+
if rec.Code != http.StatusOK {
|
|
175
|
+
t.Errorf("GET /healthz status = %d, want %d", rec.Code, http.StatusOK)
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
var body map[string]string
|
|
179
|
+
if err := json.NewDecoder(rec.Body).Decode(&body); err != nil {
|
|
180
|
+
t.Fatalf("decoding response body: %v", err)
|
|
181
|
+
}
|
|
182
|
+
if body["status"] != "ok" {
|
|
183
|
+
t.Errorf("status = %q, want %q", body["status"], "ok")
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
### Integration Tests with Build Tags
|
|
189
|
+
|
|
190
|
+
```go
|
|
191
|
+
//go:build integration
|
|
192
|
+
|
|
193
|
+
package repository_test
|
|
194
|
+
|
|
195
|
+
func TestPostgresUserRepo_Create(t *testing.T) {
|
|
196
|
+
if testing.Short() {
|
|
197
|
+
t.Skip("skipping integration test in short mode")
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
db := setupTestPostgres(t)
|
|
201
|
+
repo := NewUserRepo(db)
|
|
202
|
+
|
|
203
|
+
user := &User{Name: "Alice", Email: "alice@example.com"}
|
|
204
|
+
err := repo.Create(context.Background(), user)
|
|
205
|
+
if err != nil {
|
|
206
|
+
t.Fatalf("Create: %v", err)
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
got, err := repo.GetByID(context.Background(), user.ID)
|
|
210
|
+
if err != nil {
|
|
211
|
+
t.Fatalf("GetByID: %v", err)
|
|
212
|
+
}
|
|
213
|
+
if got.Email != user.Email {
|
|
214
|
+
t.Errorf("email = %q, want %q", got.Email, user.Email)
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
### Testing Error Cases
|
|
220
|
+
|
|
221
|
+
```go
|
|
222
|
+
func TestService_Create_ValidationErrors(t *testing.T) {
|
|
223
|
+
svc := NewService(newFakeRepo())
|
|
224
|
+
|
|
225
|
+
tests := []struct {
|
|
226
|
+
name string
|
|
227
|
+
input CreateInput
|
|
228
|
+
want error
|
|
229
|
+
}{
|
|
230
|
+
{
|
|
231
|
+
name: "empty name",
|
|
232
|
+
input: CreateInput{Name: "", Email: "a@b.com"},
|
|
233
|
+
want: ErrNameRequired,
|
|
234
|
+
},
|
|
235
|
+
{
|
|
236
|
+
name: "invalid email",
|
|
237
|
+
input: CreateInput{Name: "Alice", Email: "not-an-email"},
|
|
238
|
+
want: ErrInvalidEmail,
|
|
239
|
+
},
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
for _, tt := range tests {
|
|
243
|
+
t.Run(tt.name, func(t *testing.T) {
|
|
244
|
+
_, err := svc.Create(context.Background(), tt.input)
|
|
245
|
+
if !errors.Is(err, tt.want) {
|
|
246
|
+
t.Errorf("Create() error = %v, want %v", err, tt.want)
|
|
247
|
+
}
|
|
248
|
+
})
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
## go-cmp for Comparisons
|
|
254
|
+
|
|
255
|
+
```go
|
|
256
|
+
import "github.com/google/go-cmp/cmp"
|
|
257
|
+
|
|
258
|
+
func TestTransform(t *testing.T) {
|
|
259
|
+
got := Transform(input)
|
|
260
|
+
want := Expected{
|
|
261
|
+
Name: "Alice",
|
|
262
|
+
Items: []string{"a", "b", "c"},
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
if diff := cmp.Diff(want, got); diff != "" {
|
|
266
|
+
t.Errorf("Transform() mismatch (-want +got):\n%s", diff)
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// Ignore unexported fields or specific fields
|
|
271
|
+
if diff := cmp.Diff(want, got, cmpopts.IgnoreFields(User{}, "CreatedAt")); diff != "" {
|
|
272
|
+
t.Errorf("mismatch (-want +got):\n%s", diff)
|
|
273
|
+
}
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
## Benchmarks
|
|
277
|
+
|
|
278
|
+
```go
|
|
279
|
+
func BenchmarkProcess(b *testing.B) {
|
|
280
|
+
input := generateInput(1000)
|
|
281
|
+
|
|
282
|
+
b.ResetTimer()
|
|
283
|
+
for b.Loop() {
|
|
284
|
+
Process(input)
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// Run: go test -bench=BenchmarkProcess -benchmem ./...
|
|
289
|
+
// Output: BenchmarkProcess-8 15234 78432 ns/op 4096 B/op 12 allocs/op
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
## Race Detection
|
|
293
|
+
|
|
294
|
+
```go
|
|
295
|
+
// Always run tests with race detector in CI
|
|
296
|
+
// go test -race ./...
|
|
297
|
+
|
|
298
|
+
// The race detector finds data races at runtime.
|
|
299
|
+
// It has zero false positives — every report is a real bug.
|
|
300
|
+
// It does NOT find all races — absence of reports doesn't prove safety.
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
## Test Anti-Patterns
|
|
304
|
+
|
|
305
|
+
```go
|
|
306
|
+
// Never: Tests that depend on execution order
|
|
307
|
+
// Each test must be independently runnable
|
|
308
|
+
|
|
309
|
+
// Never: Sleeping for synchronization
|
|
310
|
+
time.Sleep(500 * time.Millisecond) // Flaky — use channels, WaitGroups, or polling
|
|
311
|
+
|
|
312
|
+
// Never: Testing unexported functions directly
|
|
313
|
+
// Test through the public API — if you can't, the API design needs work
|
|
314
|
+
|
|
315
|
+
// Never: Excessive mocking
|
|
316
|
+
// If you're mocking more than one or two dependencies, the unit is too large
|
|
317
|
+
|
|
318
|
+
// Never: Ignoring the -race flag
|
|
319
|
+
// A test suite without -race is incomplete
|
|
320
|
+
|
|
321
|
+
// Never: t.Error when you should t.Fatal
|
|
322
|
+
// If subsequent code will panic on the error condition, use t.Fatal
|
|
323
|
+
if err != nil {
|
|
324
|
+
t.Fatalf("setup failed: %v", err) // Not t.Errorf — continuing will panic
|
|
325
|
+
}
|
|
326
|
+
```
|
|
@@ -0,0 +1,361 @@
|
|
|
1
|
+
# Go Expert Development Guide
|
|
2
|
+
|
|
3
|
+
Principal-level guidelines for Go engineering. Idiomatic Go, production systems, and deep runtime knowledge.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Overview
|
|
8
|
+
|
|
9
|
+
This guide applies to:
|
|
10
|
+
- HTTP services and gRPC servers
|
|
11
|
+
- CLI tools and system utilities
|
|
12
|
+
- Libraries and shared packages
|
|
13
|
+
- Distributed systems and microservices
|
|
14
|
+
- Data pipelines and stream processing
|
|
15
|
+
- Infrastructure tooling and platform code
|
|
16
|
+
|
|
17
|
+
### Core Philosophy
|
|
18
|
+
|
|
19
|
+
Go is a language of restraint. The best Go code is boring Go code.
|
|
20
|
+
|
|
21
|
+
- **Simplicity is the highest virtue.** If a junior engineer can't read it, it's too clever.
|
|
22
|
+
- **The standard library is your first dependency.** Reach for third-party packages only when the stdlib genuinely falls short.
|
|
23
|
+
- **Errors are values, not exceptions.** Handle them explicitly at every call site.
|
|
24
|
+
- **Concurrency is a tool, not a default.** Don't use goroutines because you can — use them because the problem demands it.
|
|
25
|
+
- **Interfaces are discovered, not designed.** Accept interfaces, return structs.
|
|
26
|
+
- **If you don't know, say you don't know.** Guessing at behavior you haven't verified is worse than admitting uncertainty.
|
|
27
|
+
|
|
28
|
+
### Key Principles
|
|
29
|
+
|
|
30
|
+
1. **Effective Go Is the Baseline** — The official docs are the floor, not the ceiling
|
|
31
|
+
2. **Accept Interfaces, Return Structs** — Narrow inputs, concrete outputs
|
|
32
|
+
3. **Zero Values Are Useful** — Design types so the zero value is valid
|
|
33
|
+
4. **Error Handling Is Not Boilerplate** — Every check is a conscious decision
|
|
34
|
+
5. **Package Design Matters** — A package name IS the documentation
|
|
35
|
+
|
|
36
|
+
### Project Structure
|
|
37
|
+
|
|
38
|
+
```
|
|
39
|
+
project/
|
|
40
|
+
├── cmd/ # Main applications (wiring only)
|
|
41
|
+
│ └── myapp/main.go
|
|
42
|
+
├── internal/ # Private application code
|
|
43
|
+
│ ├── domain/ # Core business types and logic
|
|
44
|
+
│ ├── service/ # Application services / use cases
|
|
45
|
+
│ ├── repository/ # Data access implementations
|
|
46
|
+
│ ├── handler/ # HTTP/gRPC handlers
|
|
47
|
+
│ └── platform/ # Infrastructure (logging, metrics, config)
|
|
48
|
+
├── pkg/ # Public library code (use sparingly)
|
|
49
|
+
├── api/ # API definitions (OpenAPI, protobuf)
|
|
50
|
+
├── migrations/ # Database migrations
|
|
51
|
+
├── testdata/ # Test fixtures
|
|
52
|
+
├── go.mod
|
|
53
|
+
└── Makefile
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
---
|
|
57
|
+
|
|
58
|
+
## Error Handling
|
|
59
|
+
|
|
60
|
+
Errors are the primary control flow for failure cases.
|
|
61
|
+
|
|
62
|
+
### Wrap With Context
|
|
63
|
+
|
|
64
|
+
```go
|
|
65
|
+
result, err := db.QueryContext(ctx, query, args...)
|
|
66
|
+
if err != nil {
|
|
67
|
+
return fmt.Errorf("querying users by email %q: %w", email, err)
|
|
68
|
+
}
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
### Sentinel Errors and Custom Types
|
|
72
|
+
|
|
73
|
+
```go
|
|
74
|
+
var ErrNotFound = errors.New("not found")
|
|
75
|
+
|
|
76
|
+
if errors.Is(err, ErrNotFound) {
|
|
77
|
+
http.Error(w, "not found", http.StatusNotFound)
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
type ValidationError struct {
|
|
81
|
+
Field string
|
|
82
|
+
Message string
|
|
83
|
+
}
|
|
84
|
+
func (e *ValidationError) Error() string {
|
|
85
|
+
return fmt.Sprintf("validation failed on %s: %s", e.Field, e.Message)
|
|
86
|
+
}
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
### The run() Pattern
|
|
90
|
+
|
|
91
|
+
```go
|
|
92
|
+
func main() {
|
|
93
|
+
if err := run(); err != nil {
|
|
94
|
+
fmt.Fprintf(os.Stderr, "error: %v\n", err)
|
|
95
|
+
os.Exit(1)
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
---
|
|
101
|
+
|
|
102
|
+
## Concurrency
|
|
103
|
+
|
|
104
|
+
### Rules
|
|
105
|
+
|
|
106
|
+
- Don't start a goroutine you can't stop
|
|
107
|
+
- The caller decides concurrency
|
|
108
|
+
- Share memory by communicating
|
|
109
|
+
- Always select on `ctx.Done()`
|
|
110
|
+
|
|
111
|
+
### errgroup
|
|
112
|
+
|
|
113
|
+
```go
|
|
114
|
+
g, ctx := errgroup.WithContext(ctx)
|
|
115
|
+
for i, url := range urls {
|
|
116
|
+
g.Go(func() error {
|
|
117
|
+
resp, err := fetch(ctx, url)
|
|
118
|
+
if err != nil { return err }
|
|
119
|
+
responses[i] = resp
|
|
120
|
+
return nil
|
|
121
|
+
})
|
|
122
|
+
}
|
|
123
|
+
if err := g.Wait(); err != nil { return nil, err }
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
### Bounded Concurrency
|
|
127
|
+
|
|
128
|
+
```go
|
|
129
|
+
sem := make(chan struct{}, maxConcurrent)
|
|
130
|
+
g, ctx := errgroup.WithContext(ctx)
|
|
131
|
+
for _, item := range items {
|
|
132
|
+
g.Go(func() error {
|
|
133
|
+
sem <- struct{}{}
|
|
134
|
+
defer func() { <-sem }()
|
|
135
|
+
return process(ctx, item)
|
|
136
|
+
})
|
|
137
|
+
}
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
---
|
|
141
|
+
|
|
142
|
+
## Interfaces and Types
|
|
143
|
+
|
|
144
|
+
### Small Interfaces
|
|
145
|
+
|
|
146
|
+
```go
|
|
147
|
+
type Reader interface { Read(p []byte) (n int, err error) }
|
|
148
|
+
type Writer interface { Write(p []byte) (n int, err error) }
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
### Define Interfaces at the Consumer
|
|
152
|
+
|
|
153
|
+
```go
|
|
154
|
+
// In package "handler":
|
|
155
|
+
type UserFinder interface {
|
|
156
|
+
FindByID(ctx context.Context, id string) (*User, error)
|
|
157
|
+
}
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
### Functional Options
|
|
161
|
+
|
|
162
|
+
```go
|
|
163
|
+
type Option func(*Server)
|
|
164
|
+
|
|
165
|
+
func WithReadTimeout(d time.Duration) Option {
|
|
166
|
+
return func(s *Server) { s.readTimeout = d }
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
func NewServer(addr string, opts ...Option) *Server {
|
|
170
|
+
s := &Server{addr: addr, readTimeout: 30 * time.Second}
|
|
171
|
+
for _, opt := range opts { opt(s) }
|
|
172
|
+
return s
|
|
173
|
+
}
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
### Generics
|
|
177
|
+
|
|
178
|
+
```go
|
|
179
|
+
func Map[T, U any](s []T, f func(T) U) []U {
|
|
180
|
+
result := make([]U, len(s))
|
|
181
|
+
for i, v := range s { result[i] = f(v) }
|
|
182
|
+
return result
|
|
183
|
+
}
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
---
|
|
187
|
+
|
|
188
|
+
## Testing
|
|
189
|
+
|
|
190
|
+
### Table-Driven Tests
|
|
191
|
+
|
|
192
|
+
```go
|
|
193
|
+
func TestParseAge(t *testing.T) {
|
|
194
|
+
tests := []struct {
|
|
195
|
+
name string
|
|
196
|
+
input string
|
|
197
|
+
want int
|
|
198
|
+
wantErr bool
|
|
199
|
+
}{
|
|
200
|
+
{name: "valid age", input: "25", want: 25},
|
|
201
|
+
{name: "negative", input: "-1", wantErr: true},
|
|
202
|
+
{name: "non-numeric", input: "abc", wantErr: true},
|
|
203
|
+
}
|
|
204
|
+
for _, tt := range tests {
|
|
205
|
+
t.Run(tt.name, func(t *testing.T) {
|
|
206
|
+
got, err := ParseAge(tt.input)
|
|
207
|
+
if tt.wantErr {
|
|
208
|
+
if err == nil { t.Errorf("expected error") }
|
|
209
|
+
return
|
|
210
|
+
}
|
|
211
|
+
if err != nil { t.Fatalf("unexpected error: %v", err) }
|
|
212
|
+
if got != tt.want { t.Errorf("got %d, want %d", got, tt.want) }
|
|
213
|
+
})
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
### Dependency Injection via Interfaces
|
|
219
|
+
|
|
220
|
+
```go
|
|
221
|
+
type fakeUserStore struct {
|
|
222
|
+
users map[string]*User
|
|
223
|
+
err error
|
|
224
|
+
}
|
|
225
|
+
func (f *fakeUserStore) GetByID(_ context.Context, id string) (*User, error) {
|
|
226
|
+
if f.err != nil { return nil, f.err }
|
|
227
|
+
u, ok := f.users[id]
|
|
228
|
+
if !ok { return nil, ErrNotFound }
|
|
229
|
+
return u, nil
|
|
230
|
+
}
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
### HTTP Handler Tests
|
|
234
|
+
|
|
235
|
+
```go
|
|
236
|
+
req := httptest.NewRequest("GET", "/users/123", nil)
|
|
237
|
+
rec := httptest.NewRecorder()
|
|
238
|
+
srv.ServeHTTP(rec, req)
|
|
239
|
+
if rec.Code != http.StatusOK {
|
|
240
|
+
t.Errorf("status = %d, want %d", rec.Code, http.StatusOK)
|
|
241
|
+
}
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
### Race Detection
|
|
245
|
+
|
|
246
|
+
Always run `go test -race ./...` in CI. Every report is a real bug.
|
|
247
|
+
|
|
248
|
+
---
|
|
249
|
+
|
|
250
|
+
## Standard Library and Tooling
|
|
251
|
+
|
|
252
|
+
### net/http (Production-Ready)
|
|
253
|
+
|
|
254
|
+
```go
|
|
255
|
+
mux := http.NewServeMux()
|
|
256
|
+
mux.HandleFunc("GET /users/{id}", getUser)
|
|
257
|
+
|
|
258
|
+
srv := &http.Server{
|
|
259
|
+
Addr: ":8080",
|
|
260
|
+
Handler: mux,
|
|
261
|
+
ReadTimeout: 10 * time.Second,
|
|
262
|
+
WriteTimeout: 10 * time.Second,
|
|
263
|
+
}
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
### log/slog
|
|
267
|
+
|
|
268
|
+
```go
|
|
269
|
+
logger := slog.New(slog.NewJSONHandler(os.Stdout, nil))
|
|
270
|
+
logger.Info("request", "method", r.Method, "path", r.URL.Path, "duration", elapsed)
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
### Essential Commands
|
|
274
|
+
|
|
275
|
+
```bash
|
|
276
|
+
go test -race ./... # Tests with race detection
|
|
277
|
+
go vet ./... # Static analysis
|
|
278
|
+
golangci-lint run # Comprehensive linting
|
|
279
|
+
go mod tidy # Clean up dependencies
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
---
|
|
283
|
+
|
|
284
|
+
## Performance
|
|
285
|
+
|
|
286
|
+
### Profile First
|
|
287
|
+
|
|
288
|
+
```go
|
|
289
|
+
import _ "net/http/pprof"
|
|
290
|
+
// go tool pprof http://localhost:6060/debug/pprof/heap
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
### Key Patterns
|
|
294
|
+
|
|
295
|
+
- Preallocate slices when length is known
|
|
296
|
+
- Use `sync.Pool` for large, frequently allocated buffers
|
|
297
|
+
- Use `strings.Builder` for concatenation
|
|
298
|
+
- Use `sync.RWMutex` for read-heavy workloads
|
|
299
|
+
- Use `atomic` for hot counters
|
|
300
|
+
- Always set `http.Server` timeouts
|
|
301
|
+
- Always set database connection pool limits
|
|
302
|
+
|
|
303
|
+
---
|
|
304
|
+
|
|
305
|
+
## Production Patterns
|
|
306
|
+
|
|
307
|
+
### Graceful Shutdown
|
|
308
|
+
|
|
309
|
+
```go
|
|
310
|
+
ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
|
|
311
|
+
defer cancel()
|
|
312
|
+
// Start server, then <-ctx.Done(), then srv.Shutdown(shutdownCtx)
|
|
313
|
+
```
|
|
314
|
+
|
|
315
|
+
### Health Checks
|
|
316
|
+
|
|
317
|
+
```go
|
|
318
|
+
mux.HandleFunc("GET /healthz", func(w http.ResponseWriter, r *http.Request) {
|
|
319
|
+
if err := db.PingContext(r.Context()); err != nil {
|
|
320
|
+
http.Error(w, "unhealthy", http.StatusServiceUnavailable)
|
|
321
|
+
return
|
|
322
|
+
}
|
|
323
|
+
w.Write([]byte(`{"status":"ok"}`))
|
|
324
|
+
})
|
|
325
|
+
```
|
|
326
|
+
|
|
327
|
+
### Retry with Backoff
|
|
328
|
+
|
|
329
|
+
Exponential backoff with jitter. Always respect context cancellation.
|
|
330
|
+
|
|
331
|
+
### Database Transactions
|
|
332
|
+
|
|
333
|
+
```go
|
|
334
|
+
func WithTransaction(ctx context.Context, db *sql.DB, fn TxFunc) error {
|
|
335
|
+
tx, err := db.BeginTx(ctx, nil)
|
|
336
|
+
if err != nil { return err }
|
|
337
|
+
if err := fn(ctx, tx); err != nil {
|
|
338
|
+
tx.Rollback()
|
|
339
|
+
return err
|
|
340
|
+
}
|
|
341
|
+
return tx.Commit()
|
|
342
|
+
}
|
|
343
|
+
```
|
|
344
|
+
|
|
345
|
+
---
|
|
346
|
+
|
|
347
|
+
## Definition of Done
|
|
348
|
+
|
|
349
|
+
A Go feature is complete when:
|
|
350
|
+
|
|
351
|
+
- [ ] `go vet ./...` reports zero issues
|
|
352
|
+
- [ ] `staticcheck ./...` reports zero issues
|
|
353
|
+
- [ ] `golangci-lint run` passes
|
|
354
|
+
- [ ] `go test -race ./...` passes with no race conditions
|
|
355
|
+
- [ ] Test coverage covers meaningful behavior
|
|
356
|
+
- [ ] Error paths are tested
|
|
357
|
+
- [ ] Documentation comments on all exported identifiers
|
|
358
|
+
- [ ] Context propagation is correct
|
|
359
|
+
- [ ] Resource cleanup verified (deferred closes, connection pools)
|
|
360
|
+
- [ ] No `TODO` without an associated issue
|
|
361
|
+
- [ ] Code reviewed and approved
|