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,320 @@
|
|
|
1
|
+
# Go Production Patterns
|
|
2
|
+
|
|
3
|
+
Patterns for building Go services that run reliably in production.
|
|
4
|
+
|
|
5
|
+
## Application Lifecycle
|
|
6
|
+
|
|
7
|
+
### The run() Pattern
|
|
8
|
+
|
|
9
|
+
```go
|
|
10
|
+
func main() {
|
|
11
|
+
ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
|
|
12
|
+
defer cancel()
|
|
13
|
+
|
|
14
|
+
if err := run(ctx, os.Stdout, os.Args[1:]); err != nil {
|
|
15
|
+
fmt.Fprintf(os.Stderr, "error: %v\n", err)
|
|
16
|
+
os.Exit(1)
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
func run(ctx context.Context, stdout io.Writer, args []string) error {
|
|
21
|
+
cfg, err := loadConfig()
|
|
22
|
+
if err != nil {
|
|
23
|
+
return fmt.Errorf("loading config: %w", err)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
logger := slog.New(slog.NewJSONHandler(stdout, nil))
|
|
27
|
+
db, err := connectDB(ctx, cfg.DatabaseURL)
|
|
28
|
+
if err != nil {
|
|
29
|
+
return fmt.Errorf("connecting to database: %w", err)
|
|
30
|
+
}
|
|
31
|
+
defer db.Close()
|
|
32
|
+
|
|
33
|
+
srv := NewServer(logger, db)
|
|
34
|
+
httpSrv := &http.Server{
|
|
35
|
+
Addr: cfg.Addr,
|
|
36
|
+
Handler: srv,
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Start server in background
|
|
40
|
+
go func() {
|
|
41
|
+
logger.Info("server starting", "addr", cfg.Addr)
|
|
42
|
+
if err := httpSrv.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
|
|
43
|
+
logger.Error("server error", "error", err)
|
|
44
|
+
}
|
|
45
|
+
}()
|
|
46
|
+
|
|
47
|
+
// Wait for shutdown signal
|
|
48
|
+
<-ctx.Done()
|
|
49
|
+
logger.Info("shutting down")
|
|
50
|
+
|
|
51
|
+
shutdownCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
|
52
|
+
defer cancel()
|
|
53
|
+
|
|
54
|
+
return httpSrv.Shutdown(shutdownCtx)
|
|
55
|
+
}
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
### Dependency Injection
|
|
59
|
+
|
|
60
|
+
```go
|
|
61
|
+
// Wire dependencies explicitly in main/run — no magic, no frameworks
|
|
62
|
+
func run(ctx context.Context) error {
|
|
63
|
+
// Infrastructure
|
|
64
|
+
db := connectDB(cfg.DatabaseURL)
|
|
65
|
+
cache := redis.New(cfg.RedisURL)
|
|
66
|
+
|
|
67
|
+
// Repositories
|
|
68
|
+
userRepo := postgres.NewUserRepo(db)
|
|
69
|
+
orderRepo := postgres.NewOrderRepo(db)
|
|
70
|
+
|
|
71
|
+
// Services
|
|
72
|
+
userSvc := service.NewUserService(userRepo, cache)
|
|
73
|
+
orderSvc := service.NewOrderService(orderRepo, userSvc)
|
|
74
|
+
|
|
75
|
+
// Handlers
|
|
76
|
+
handler := handler.New(userSvc, orderSvc)
|
|
77
|
+
|
|
78
|
+
// Server
|
|
79
|
+
srv := &http.Server{Handler: handler}
|
|
80
|
+
return srv.ListenAndServe()
|
|
81
|
+
}
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
## Configuration
|
|
85
|
+
|
|
86
|
+
```go
|
|
87
|
+
// Validate all configuration at startup — fail fast
|
|
88
|
+
type Config struct {
|
|
89
|
+
Addr string `env:"ADDR" default:":8080"`
|
|
90
|
+
DatabaseURL string `env:"DATABASE_URL,required"`
|
|
91
|
+
LogLevel slog.Level `env:"LOG_LEVEL" default:"info"`
|
|
92
|
+
Timeout time.Duration `env:"TIMEOUT" default:"30s"`
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
func loadConfig() (*Config, error) {
|
|
96
|
+
cfg := &Config{}
|
|
97
|
+
// Parse from env...
|
|
98
|
+
|
|
99
|
+
// Validate
|
|
100
|
+
if cfg.DatabaseURL == "" {
|
|
101
|
+
return nil, errors.New("DATABASE_URL is required")
|
|
102
|
+
}
|
|
103
|
+
if cfg.Timeout <= 0 {
|
|
104
|
+
return nil, errors.New("TIMEOUT must be positive")
|
|
105
|
+
}
|
|
106
|
+
return cfg, nil
|
|
107
|
+
}
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
## Health Checks
|
|
111
|
+
|
|
112
|
+
```go
|
|
113
|
+
func (s *Server) handleHealthz(w http.ResponseWriter, r *http.Request) {
|
|
114
|
+
ctx, cancel := context.WithTimeout(r.Context(), 2*time.Second)
|
|
115
|
+
defer cancel()
|
|
116
|
+
|
|
117
|
+
checks := map[string]error{
|
|
118
|
+
"database": s.db.PingContext(ctx),
|
|
119
|
+
"cache": s.cache.Ping(ctx),
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
status := http.StatusOK
|
|
123
|
+
result := make(map[string]string, len(checks))
|
|
124
|
+
for name, err := range checks {
|
|
125
|
+
if err != nil {
|
|
126
|
+
status = http.StatusServiceUnavailable
|
|
127
|
+
result[name] = err.Error()
|
|
128
|
+
} else {
|
|
129
|
+
result[name] = "ok"
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
w.Header().Set("Content-Type", "application/json")
|
|
134
|
+
w.WriteHeader(status)
|
|
135
|
+
json.NewEncoder(w).Encode(result)
|
|
136
|
+
}
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
## Observability
|
|
140
|
+
|
|
141
|
+
### Structured Logging
|
|
142
|
+
|
|
143
|
+
```go
|
|
144
|
+
// Use slog with consistent field names
|
|
145
|
+
logger.Info("request completed",
|
|
146
|
+
"method", r.Method,
|
|
147
|
+
"path", r.URL.Path,
|
|
148
|
+
"status", status,
|
|
149
|
+
"duration_ms", time.Since(start).Milliseconds(),
|
|
150
|
+
"request_id", middleware.RequestID(r.Context()),
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
// Log levels have meaning:
|
|
154
|
+
// Debug: Development-only information
|
|
155
|
+
// Info: Normal operations worth recording
|
|
156
|
+
// Warn: Something unexpected but handled
|
|
157
|
+
// Error: Something failed and needs attention
|
|
158
|
+
|
|
159
|
+
// Never log sensitive data (passwords, tokens, PII)
|
|
160
|
+
// Never log at Error level for expected conditions (404, validation failures)
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
### Metrics
|
|
164
|
+
|
|
165
|
+
```go
|
|
166
|
+
import "github.com/prometheus/client_golang/prometheus"
|
|
167
|
+
|
|
168
|
+
var (
|
|
169
|
+
httpRequestsTotal = prometheus.NewCounterVec(
|
|
170
|
+
prometheus.CounterOpts{
|
|
171
|
+
Name: "http_requests_total",
|
|
172
|
+
Help: "Total number of HTTP requests",
|
|
173
|
+
},
|
|
174
|
+
[]string{"method", "path", "status"},
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
httpRequestDuration = prometheus.NewHistogramVec(
|
|
178
|
+
prometheus.HistogramOpts{
|
|
179
|
+
Name: "http_request_duration_seconds",
|
|
180
|
+
Help: "HTTP request duration in seconds",
|
|
181
|
+
Buckets: prometheus.DefBuckets,
|
|
182
|
+
},
|
|
183
|
+
[]string{"method", "path"},
|
|
184
|
+
)
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
func init() {
|
|
188
|
+
prometheus.MustRegister(httpRequestsTotal, httpRequestDuration)
|
|
189
|
+
}
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
## Graceful Degradation
|
|
193
|
+
|
|
194
|
+
```go
|
|
195
|
+
// Circuit breaker for external dependencies
|
|
196
|
+
type CircuitBreaker struct {
|
|
197
|
+
mu sync.Mutex
|
|
198
|
+
failures int
|
|
199
|
+
threshold int
|
|
200
|
+
resetAfter time.Duration
|
|
201
|
+
lastFailure time.Time
|
|
202
|
+
state string // "closed", "open", "half-open"
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
func (cb *CircuitBreaker) Execute(fn func() error) error {
|
|
206
|
+
cb.mu.Lock()
|
|
207
|
+
if cb.state == "open" {
|
|
208
|
+
if time.Since(cb.lastFailure) > cb.resetAfter {
|
|
209
|
+
cb.state = "half-open"
|
|
210
|
+
} else {
|
|
211
|
+
cb.mu.Unlock()
|
|
212
|
+
return ErrCircuitOpen
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
cb.mu.Unlock()
|
|
216
|
+
|
|
217
|
+
err := fn()
|
|
218
|
+
|
|
219
|
+
cb.mu.Lock()
|
|
220
|
+
defer cb.mu.Unlock()
|
|
221
|
+
if err != nil {
|
|
222
|
+
cb.failures++
|
|
223
|
+
cb.lastFailure = time.Now()
|
|
224
|
+
if cb.failures >= cb.threshold {
|
|
225
|
+
cb.state = "open"
|
|
226
|
+
}
|
|
227
|
+
return err
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
cb.failures = 0
|
|
231
|
+
cb.state = "closed"
|
|
232
|
+
return nil
|
|
233
|
+
}
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
## Retry with Backoff
|
|
237
|
+
|
|
238
|
+
```go
|
|
239
|
+
func RetryWithBackoff(ctx context.Context, maxAttempts int, base time.Duration, fn func() error) error {
|
|
240
|
+
var err error
|
|
241
|
+
for attempt := range maxAttempts {
|
|
242
|
+
err = fn()
|
|
243
|
+
if err == nil {
|
|
244
|
+
return nil
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
if attempt == maxAttempts-1 {
|
|
248
|
+
break
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// Exponential backoff with jitter
|
|
252
|
+
backoff := base * time.Duration(1<<uint(attempt))
|
|
253
|
+
jitter := time.Duration(rand.Int64N(int64(backoff) / 2))
|
|
254
|
+
wait := backoff + jitter
|
|
255
|
+
|
|
256
|
+
select {
|
|
257
|
+
case <-ctx.Done():
|
|
258
|
+
return ctx.Err()
|
|
259
|
+
case <-time.After(wait):
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
return fmt.Errorf("after %d attempts: %w", maxAttempts, err)
|
|
263
|
+
}
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
## Database Patterns
|
|
267
|
+
|
|
268
|
+
```go
|
|
269
|
+
// Repository pattern with transactions
|
|
270
|
+
type TxFunc func(ctx context.Context, tx *sql.Tx) error
|
|
271
|
+
|
|
272
|
+
func WithTransaction(ctx context.Context, db *sql.DB, fn TxFunc) error {
|
|
273
|
+
tx, err := db.BeginTx(ctx, nil)
|
|
274
|
+
if err != nil {
|
|
275
|
+
return fmt.Errorf("beginning transaction: %w", err)
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
if err := fn(ctx, tx); err != nil {
|
|
279
|
+
if rbErr := tx.Rollback(); rbErr != nil {
|
|
280
|
+
return fmt.Errorf("rolling back: %w (original: %v)", rbErr, err)
|
|
281
|
+
}
|
|
282
|
+
return err
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
if err := tx.Commit(); err != nil {
|
|
286
|
+
return fmt.Errorf("committing transaction: %w", err)
|
|
287
|
+
}
|
|
288
|
+
return nil
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// Usage
|
|
292
|
+
err := WithTransaction(ctx, db, func(ctx context.Context, tx *sql.Tx) error {
|
|
293
|
+
if err := createOrder(ctx, tx, order); err != nil {
|
|
294
|
+
return err
|
|
295
|
+
}
|
|
296
|
+
return updateInventory(ctx, tx, order.Items)
|
|
297
|
+
})
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
## Anti-Patterns
|
|
301
|
+
|
|
302
|
+
```go
|
|
303
|
+
// Never: Global mutable state
|
|
304
|
+
var db *sql.DB // Package-level mutable state makes testing impossible
|
|
305
|
+
|
|
306
|
+
// Never: init() for complex initialization
|
|
307
|
+
func init() {
|
|
308
|
+
db, _ = sql.Open(...) // Errors swallowed, untestable, order-dependent
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// Never: Shared context.Background() in request handlers
|
|
312
|
+
func handler(w http.ResponseWriter, r *http.Request) {
|
|
313
|
+
db.QueryContext(context.Background(), ...) // Ignores request cancellation!
|
|
314
|
+
// Use r.Context() instead
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// Never: Unbounded work queues
|
|
318
|
+
ch := make(chan Job) // Unbuffered or unbounded — will block or OOM
|
|
319
|
+
// Use bounded channels and handle backpressure explicitly
|
|
320
|
+
```
|
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
# Go Standard Library and Tooling
|
|
2
|
+
|
|
3
|
+
The standard library is Go's greatest asset. Know it deeply before reaching for third-party packages.
|
|
4
|
+
|
|
5
|
+
## Essential stdlib Packages
|
|
6
|
+
|
|
7
|
+
### net/http
|
|
8
|
+
|
|
9
|
+
```go
|
|
10
|
+
// The stdlib HTTP server is production-ready. You rarely need a framework.
|
|
11
|
+
mux := http.NewServeMux()
|
|
12
|
+
|
|
13
|
+
mux.HandleFunc("GET /users/{id}", getUser)
|
|
14
|
+
mux.HandleFunc("POST /users", createUser)
|
|
15
|
+
mux.HandleFunc("GET /healthz", healthCheck)
|
|
16
|
+
|
|
17
|
+
srv := &http.Server{
|
|
18
|
+
Addr: ":8080",
|
|
19
|
+
Handler: mux,
|
|
20
|
+
ReadTimeout: 10 * time.Second,
|
|
21
|
+
WriteTimeout: 10 * time.Second,
|
|
22
|
+
IdleTimeout: 120 * time.Second,
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Always set timeouts — an http.Server with no timeouts will leak goroutines
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
### Middleware Pattern
|
|
29
|
+
|
|
30
|
+
```go
|
|
31
|
+
// Middleware is just a function that wraps an http.Handler
|
|
32
|
+
func Logging(logger *slog.Logger) func(http.Handler) http.Handler {
|
|
33
|
+
return func(next http.Handler) http.Handler {
|
|
34
|
+
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
35
|
+
start := time.Now()
|
|
36
|
+
wrapped := &responseRecorder{ResponseWriter: w, statusCode: http.StatusOK}
|
|
37
|
+
|
|
38
|
+
next.ServeHTTP(wrapped, r)
|
|
39
|
+
|
|
40
|
+
logger.Info("request",
|
|
41
|
+
"method", r.Method,
|
|
42
|
+
"path", r.URL.Path,
|
|
43
|
+
"status", wrapped.statusCode,
|
|
44
|
+
"duration", time.Since(start),
|
|
45
|
+
)
|
|
46
|
+
})
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
type responseRecorder struct {
|
|
51
|
+
http.ResponseWriter
|
|
52
|
+
statusCode int
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
func (r *responseRecorder) WriteHeader(code int) {
|
|
56
|
+
r.statusCode = code
|
|
57
|
+
r.ResponseWriter.WriteHeader(code)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Chain middleware
|
|
61
|
+
handler := Logging(logger)(Recovery()(Auth(secret)(mux)))
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
### encoding/json
|
|
65
|
+
|
|
66
|
+
```go
|
|
67
|
+
// Struct tags control serialization
|
|
68
|
+
type User struct {
|
|
69
|
+
ID string `json:"id"`
|
|
70
|
+
Email string `json:"email"`
|
|
71
|
+
Name string `json:"name,omitempty"`
|
|
72
|
+
CreatedAt time.Time `json:"created_at"`
|
|
73
|
+
password string // unexported — never serialized
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Use json.NewDecoder for streams, json.Unmarshal for []byte
|
|
77
|
+
func decodeBody(r *http.Request, v any) error {
|
|
78
|
+
dec := json.NewDecoder(r.Body)
|
|
79
|
+
dec.DisallowUnknownFields() // Reject unexpected fields
|
|
80
|
+
if err := dec.Decode(v); err != nil {
|
|
81
|
+
return fmt.Errorf("decoding request body: %w", err)
|
|
82
|
+
}
|
|
83
|
+
return nil
|
|
84
|
+
}
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
### log/slog (Go 1.21+)
|
|
88
|
+
|
|
89
|
+
```go
|
|
90
|
+
// slog is the standard structured logger — use it
|
|
91
|
+
logger := slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
|
|
92
|
+
Level: slog.LevelInfo,
|
|
93
|
+
}))
|
|
94
|
+
|
|
95
|
+
logger.Info("server starting", "addr", addr, "version", version)
|
|
96
|
+
logger.Error("query failed", "error", err, "query", query)
|
|
97
|
+
|
|
98
|
+
// Add context to a sub-logger
|
|
99
|
+
reqLogger := logger.With("request_id", requestID, "user_id", userID)
|
|
100
|
+
reqLogger.Info("processing request")
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
### context
|
|
104
|
+
|
|
105
|
+
```go
|
|
106
|
+
// Context carries deadlines, cancellation signals, and request-scoped values
|
|
107
|
+
// across API boundaries and goroutines
|
|
108
|
+
|
|
109
|
+
// Timeouts
|
|
110
|
+
ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
|
|
111
|
+
defer cancel()
|
|
112
|
+
|
|
113
|
+
result, err := db.QueryContext(ctx, query)
|
|
114
|
+
|
|
115
|
+
// Values — sparingly, for request-scoped cross-cutting concerns only
|
|
116
|
+
type contextKey string
|
|
117
|
+
const requestIDKey contextKey = "request_id"
|
|
118
|
+
|
|
119
|
+
func WithRequestID(ctx context.Context, id string) context.Context {
|
|
120
|
+
return context.WithValue(ctx, requestIDKey, id)
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
func RequestID(ctx context.Context) string {
|
|
124
|
+
id, _ := ctx.Value(requestIDKey).(string)
|
|
125
|
+
return id
|
|
126
|
+
}
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
### io and io/fs
|
|
130
|
+
|
|
131
|
+
```go
|
|
132
|
+
// io.Reader and io.Writer are the universal interfaces
|
|
133
|
+
// If your function accepts io.Reader, it works with:
|
|
134
|
+
// - files, HTTP bodies, buffers, strings, gzip streams, network connections...
|
|
135
|
+
|
|
136
|
+
func CountLines(r io.Reader) (int, error) {
|
|
137
|
+
scanner := bufio.NewScanner(r)
|
|
138
|
+
count := 0
|
|
139
|
+
for scanner.Scan() {
|
|
140
|
+
count++
|
|
141
|
+
}
|
|
142
|
+
return count, scanner.Err()
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Works with anything
|
|
146
|
+
CountLines(os.Stdin)
|
|
147
|
+
CountLines(strings.NewReader("hello\nworld"))
|
|
148
|
+
CountLines(resp.Body)
|
|
149
|
+
|
|
150
|
+
// io/fs for filesystem abstraction (Go 1.16+)
|
|
151
|
+
func LoadTemplates(fsys fs.FS) (*template.Template, error) {
|
|
152
|
+
return template.ParseFS(fsys, "templates/*.html")
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Production: os.DirFS("./templates")
|
|
156
|
+
// Tests: fstest.MapFS{"templates/index.html": {Data: []byte("...")}}
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
### time
|
|
160
|
+
|
|
161
|
+
```go
|
|
162
|
+
// Always use time.Duration for durations, never raw int64
|
|
163
|
+
func Retry(attempts int, delay time.Duration, fn func() error) error { ... }
|
|
164
|
+
|
|
165
|
+
// Use time.NewTicker, not time.Tick (Tick leaks)
|
|
166
|
+
ticker := time.NewTicker(30 * time.Second)
|
|
167
|
+
defer ticker.Stop()
|
|
168
|
+
|
|
169
|
+
// Use monotonic clock for elapsed time (time.Since does this automatically)
|
|
170
|
+
start := time.Now()
|
|
171
|
+
doWork()
|
|
172
|
+
elapsed := time.Since(start) // Monotonic, not affected by clock adjustments
|
|
173
|
+
|
|
174
|
+
// Parse and format with the reference time: Mon Jan 2 15:04:05 MST 2006
|
|
175
|
+
t, err := time.Parse("2006-01-02", "2025-03-15")
|
|
176
|
+
s := t.Format(time.RFC3339)
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
## Toolchain
|
|
180
|
+
|
|
181
|
+
### Essential Commands
|
|
182
|
+
|
|
183
|
+
```bash
|
|
184
|
+
# Build and run
|
|
185
|
+
go build ./... # Compile all packages
|
|
186
|
+
go run ./cmd/myapp # Compile and execute
|
|
187
|
+
go install ./cmd/myapp # Install binary to $GOBIN
|
|
188
|
+
|
|
189
|
+
# Testing
|
|
190
|
+
go test ./... # Run all tests
|
|
191
|
+
go test -race ./... # Run with race detector (always in CI)
|
|
192
|
+
go test -count=1 ./... # Disable test caching
|
|
193
|
+
go test -short ./... # Skip long-running tests
|
|
194
|
+
go test -cover ./... # Show coverage percentage
|
|
195
|
+
go test -coverprofile=coverage.out ./... && go tool cover -html=coverage.out
|
|
196
|
+
|
|
197
|
+
# Vetting
|
|
198
|
+
go vet ./... # Official static analysis
|
|
199
|
+
|
|
200
|
+
# Modules
|
|
201
|
+
go mod tidy # Remove unused, add missing dependencies
|
|
202
|
+
go mod verify # Verify dependency checksums
|
|
203
|
+
go mod why <module> # Explain why a dependency is needed
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
### Linters
|
|
207
|
+
|
|
208
|
+
```yaml
|
|
209
|
+
# .golangci-lint.yml — recommended configuration
|
|
210
|
+
linters:
|
|
211
|
+
enable:
|
|
212
|
+
- errcheck # Check that errors are handled
|
|
213
|
+
- govet # Official Go vet checks
|
|
214
|
+
- staticcheck # The gold standard static analyzer
|
|
215
|
+
- unused # Find unused code
|
|
216
|
+
- gosimple # Simplify code
|
|
217
|
+
- ineffassign # Detect ineffectual assignments
|
|
218
|
+
- gocritic # Opinionated style checks
|
|
219
|
+
- revive # Fast, configurable linter
|
|
220
|
+
- misspell # Find common spelling mistakes
|
|
221
|
+
- prealloc # Suggest preallocating slices
|
|
222
|
+
- unconvert # Remove unnecessary type conversions
|
|
223
|
+
- errname # Check error variable naming conventions
|
|
224
|
+
- errorlint # Check error handling idioms
|
|
225
|
+
|
|
226
|
+
linters-settings:
|
|
227
|
+
errcheck:
|
|
228
|
+
check-type-assertions: true
|
|
229
|
+
check-blank: true
|
|
230
|
+
gocritic:
|
|
231
|
+
enabled-tags:
|
|
232
|
+
- diagnostic
|
|
233
|
+
- style
|
|
234
|
+
- performance
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
### Makefile
|
|
238
|
+
|
|
239
|
+
```makefile
|
|
240
|
+
.PHONY: build test lint vet check
|
|
241
|
+
|
|
242
|
+
build:
|
|
243
|
+
go build ./...
|
|
244
|
+
|
|
245
|
+
test:
|
|
246
|
+
go test -race -count=1 ./...
|
|
247
|
+
|
|
248
|
+
lint:
|
|
249
|
+
golangci-lint run
|
|
250
|
+
|
|
251
|
+
vet:
|
|
252
|
+
go vet ./...
|
|
253
|
+
staticcheck ./...
|
|
254
|
+
|
|
255
|
+
check: vet lint test
|
|
256
|
+
|
|
257
|
+
cover:
|
|
258
|
+
go test -coverprofile=coverage.out ./...
|
|
259
|
+
go tool cover -html=coverage.out -o coverage.html
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
## When to Use Third-Party Packages
|
|
263
|
+
|
|
264
|
+
Use the stdlib unless:
|
|
265
|
+
- The stdlib genuinely lacks the capability (e.g., database drivers, protocol buffers)
|
|
266
|
+
- A well-maintained package provides significantly better ergonomics for a complex domain (e.g., `sqlc` for SQL, `chi` for routing in complex APIs)
|
|
267
|
+
- The package is from the Go team's extended ecosystem (`golang.org/x/...`)
|
|
268
|
+
|
|
269
|
+
Respected packages in the ecosystem:
|
|
270
|
+
- **Router**: `net/http` (Go 1.22+ patterns), `chi` for complex routing
|
|
271
|
+
- **SQL**: `sqlc`, `pgx`, `database/sql` with driver
|
|
272
|
+
- **Testing**: `go-cmp`, `testify` (use sparingly — stdlib is usually enough)
|
|
273
|
+
- **CLI**: `cobra`, `kong`
|
|
274
|
+
- **Config**: `envconfig`, `viper`
|
|
275
|
+
- **Observability**: `prometheus/client_golang`, `opentelemetry-go`
|
|
276
|
+
- **Concurrency**: `golang.org/x/sync/errgroup`, `golang.org/x/sync/semaphore`
|