ga-plugins-cli 0.1.0 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. package/dist/config-patcher.d.ts +20 -50
  2. package/dist/config-patcher.d.ts.map +1 -1
  3. package/dist/config-patcher.js +138 -102
  4. package/dist/config-patcher.js.map +1 -1
  5. package/dist/index.js +75 -22
  6. package/dist/index.js.map +1 -1
  7. package/dist/installer.d.ts +0 -18
  8. package/dist/installer.d.ts.map +1 -1
  9. package/dist/installer.js +19 -39
  10. package/dist/installer.js.map +1 -1
  11. package/dist/types.d.ts +10 -6
  12. package/dist/types.d.ts.map +1 -1
  13. package/dist/uninstaller.d.ts +0 -23
  14. package/dist/uninstaller.d.ts.map +1 -1
  15. package/dist/uninstaller.js +22 -68
  16. package/dist/uninstaller.js.map +1 -1
  17. package/package.json +3 -2
  18. package/plugins/go-reviewer/.claude-plugin/plugin.json +12 -0
  19. package/plugins/go-reviewer/commands/go-review.md +424 -0
  20. package/plugins/go-reviewer/mcp-servers/go-reviewer-mcp/README.md +236 -0
  21. package/plugins/go-reviewer/mcp-servers/go-reviewer-mcp/main.go +678 -0
  22. package/plugins/go-scaffolder/.claude-plugin/plugin.json +12 -0
  23. package/plugins/go-scaffolder/commands/scaffold-service.md +802 -0
  24. package/plugins/go-scaffolder/reference-service/.env.example +27 -0
  25. package/plugins/go-scaffolder/reference-service/Dockerfile +55 -0
  26. package/plugins/go-scaffolder/reference-service/REFERENCE-SERVICE-NOTICE.md +104 -0
  27. package/plugins/go-scaffolder/reference-service/cmd/server/main.go +266 -0
  28. package/plugins/go-scaffolder/reference-service/config/config.go +67 -0
  29. package/plugins/go-scaffolder/reference-service/go.mod +17 -0
  30. package/plugins/go-scaffolder/reference-service/internal/domain/booking.go +118 -0
  31. package/plugins/go-scaffolder/reference-service/internal/handler/booking.go +242 -0
  32. package/plugins/go-scaffolder/reference-service/internal/handler/booking_test.go +451 -0
  33. package/plugins/go-scaffolder/reference-service/internal/repository/booking_postgres.go +124 -0
  34. package/plugins/go-scaffolder/reference-service/internal/usecase/booking.go +181 -0
  35. package/plugins/go-standards/.claude-plugin/plugin.json +22 -0
  36. package/plugins/go-standards/commands/go-standards-check.md +232 -0
  37. package/plugins/go-standards/skills/concurrency.md +336 -0
  38. package/plugins/go-standards/skills/config.md +267 -0
  39. package/plugins/go-standards/skills/error-handling.md +286 -0
  40. package/plugins/go-standards/skills/http-chi.md +390 -0
  41. package/plugins/go-standards/skills/logging-observability.md +340 -0
  42. package/plugins/go-standards/skills/naming-and-style.md +315 -0
  43. package/plugins/go-standards/skills/project-layout.md +313 -0
  44. package/plugins/go-standards/skills/testing.md +366 -0
  45. package/plugins/java2go-porter/.claude-plugin/plugin.json +21 -0
  46. package/plugins/java2go-porter/agents/analyzer.md +232 -0
  47. package/plugins/java2go-porter/agents/reviewer.md +241 -0
  48. package/plugins/java2go-porter/agents/test-pairer.md +365 -0
  49. package/plugins/java2go-porter/agents/translator.md +419 -0
  50. package/plugins/java2go-porter/commands/port-java-service.md +149 -0
  51. package/plugins/java2go-porter/skills/idiom-mapping.md +75 -0
  52. package/plugins/migration-safety/.claude-plugin/plugin.json +20 -0
  53. package/plugins/migration-safety/commands/gen-characterization-test.md +452 -0
  54. package/plugins/migration-safety/commands/strangler-plan.md +356 -0
  55. package/plugins/migration-safety/skills/strangler-fig.md +382 -0
@@ -0,0 +1,336 @@
1
+ # Concurrency Standard
2
+
3
+ Go's concurrency primitives are powerful and easy to misuse. These rules
4
+ prevent goroutine leaks, data races, and deadlocks — the three most common
5
+ concurrency bugs in migrated Java services.
6
+
7
+ ---
8
+
9
+ ## Rule 1 — Always pass `context.Context` as the first argument
10
+
11
+ Every function that does I/O, blocks, or can be cancelled must accept
12
+ `context.Context` as its first parameter. Never store a context in a struct.
13
+
14
+ ### DO
15
+
16
+ ```go
17
+ // Every layer propagates the context
18
+ func (uc *UserUsecase) Create(ctx context.Context, input CreateUserInput) (*User, error) {
19
+ return uc.repo.Create(ctx, &User{Email: input.Email})
20
+ }
21
+
22
+ func (r *userRepository) Create(ctx context.Context, u *User) (*User, error) {
23
+ _, err := r.db.ExecContext(ctx, insertUser, u.Email, u.Name)
24
+ if err != nil {
25
+ return nil, fmt.Errorf("insert user: %w", err)
26
+ }
27
+ return u, nil
28
+ }
29
+ ```
30
+
31
+ ### DO NOT
32
+
33
+ ```go
34
+ // WRONG: context stored in struct
35
+ type UserUsecase struct {
36
+ ctx context.Context // WRONG: contexts belong to calls, not structs
37
+ repo UserRepository
38
+ }
39
+
40
+ // WRONG: context.Background() created inside a function — ignores cancellation
41
+ func (r *userRepository) Create(u *User) (*User, error) {
42
+ // WRONG: creates a new root context, ignoring the caller's deadline/cancellation
43
+ _, err := r.db.ExecContext(context.Background(), insertUser, u.Email)
44
+ return u, err
45
+ }
46
+
47
+ // WRONG: context not threaded through — downstream cannot be cancelled
48
+ func (uc *UserUsecase) BulkCreate(users []User) error {
49
+ for _, u := range users {
50
+ uc.repo.Create(&u) // WRONG: no context
51
+ }
52
+ return nil
53
+ }
54
+ ```
55
+
56
+ ---
57
+
58
+ ## Rule 2 — Cancel contexts; always `defer cancel()`
59
+
60
+ Every call to `context.WithCancel`, `context.WithTimeout`, or
61
+ `context.WithDeadline` returns a cancel function. Always call it, always
62
+ with defer, to release resources.
63
+
64
+ ### DO
65
+
66
+ ```go
67
+ func (uc *UserUsecase) FetchExternal(ctx context.Context, id string) (*User, error) {
68
+ // Enforce a maximum wait time for the external call
69
+ ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
70
+ defer cancel() // releases timer resources whether we return early or not
71
+
72
+ return uc.externalClient.Get(ctx, id)
73
+ }
74
+ ```
75
+
76
+ ```go
77
+ // In main — cancel on shutdown
78
+ ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
79
+ defer stop() // always called, even if we panic-recover
80
+ ```
81
+
82
+ ### DO NOT
83
+
84
+ ```go
85
+ // WRONG: cancel never called — goroutine/timer leak
86
+ func (uc *UserUsecase) FetchExternal(ctx context.Context, id string) (*User, error) {
87
+ ctx, _ = context.WithTimeout(ctx, 5*time.Second) // WRONG: _ discards cancel
88
+ return uc.externalClient.Get(ctx, id)
89
+ }
90
+
91
+ // WRONG: cancel called only in the happy path
92
+ func process(ctx context.Context) error {
93
+ ctx, cancel := context.WithTimeout(ctx, 10*time.Second)
94
+ result, err := doWork(ctx)
95
+ if err != nil {
96
+ return err // WRONG: cancel not called on error path
97
+ }
98
+ cancel()
99
+ return use(result)
100
+ }
101
+ ```
102
+
103
+ ---
104
+
105
+ ## Rule 3 — Goroutines must always have a termination path
106
+
107
+ Every goroutine you launch must have a clear way to stop: either a
108
+ `context.Context` that will be cancelled, a done channel, or a fixed
109
+ lifetime (e.g., handles one request then exits). Goroutines that run
110
+ forever without a stop signal are leaks.
111
+
112
+ ### DO
113
+
114
+ ```go
115
+ // Background worker with context-based cancellation
116
+ func (w *Worker) Start(ctx context.Context) {
117
+ go func() {
118
+ ticker := time.NewTicker(30 * time.Second)
119
+ defer ticker.Stop()
120
+
121
+ for {
122
+ select {
123
+ case <-ctx.Done():
124
+ return // clean exit when context is cancelled
125
+ case <-ticker.C:
126
+ if err := w.runJob(ctx); err != nil {
127
+ w.logger.Error("job failed", zap.Error(err))
128
+ }
129
+ }
130
+ }
131
+ }()
132
+ }
133
+ ```
134
+
135
+ ```go
136
+ // Fire-and-forget with timeout — not truly fire-and-forget, has bounded lifetime
137
+ func sendNotificationAsync(ctx context.Context, userID string, client NotificationClient, logger *zap.Logger) {
138
+ go func() {
139
+ ctx, cancel := context.WithTimeout(ctx, 10*time.Second)
140
+ defer cancel()
141
+
142
+ if err := client.Notify(ctx, userID); err != nil {
143
+ logger.Error("async notification failed",
144
+ zap.Error(err),
145
+ zap.String("user_id", userID),
146
+ )
147
+ }
148
+ }()
149
+ }
150
+ ```
151
+
152
+ ### DO NOT
153
+
154
+ ```go
155
+ // WRONG: goroutine with no termination path
156
+ go func() {
157
+ for {
158
+ processQueue() // WRONG: runs forever, cannot be stopped for graceful shutdown
159
+ time.Sleep(time.Second)
160
+ }
161
+ }()
162
+
163
+ // WRONG: goroutine inherits a context that will be cancelled before it finishes
164
+ func (h *UserHandler) Create(w http.ResponseWriter, r *http.Request) {
165
+ user, _ := h.uc.Create(r.Context(), input)
166
+ go func() {
167
+ // WRONG: r.Context() is cancelled when the HTTP response is sent
168
+ // This goroutine will see context cancellation immediately after the handler returns
169
+ h.auditLogger.Log(r.Context(), user)
170
+ }()
171
+ }
172
+ // Fix: pass context.WithoutCancel(r.Context()) or a new background context
173
+ ```
174
+
175
+ ---
176
+
177
+ ## Rule 4 — `sync.WaitGroup` for fan-out patterns
178
+
179
+ When launching multiple goroutines that must all complete before proceeding,
180
+ use `sync.WaitGroup`. Always call `wg.Add(n)` before launching goroutines,
181
+ never inside them.
182
+
183
+ ### DO
184
+
185
+ ```go
186
+ func (uc *UserUsecase) BulkEnrich(ctx context.Context, users []*User) error {
187
+ var wg sync.WaitGroup
188
+ errs := make(chan error, len(users)) // buffered so sends never block
189
+
190
+ for _, u := range users {
191
+ wg.Add(1)
192
+ go func(user *User) {
193
+ defer wg.Done()
194
+ if err := uc.enrich(ctx, user); err != nil {
195
+ errs <- fmt.Errorf("enrich user %s: %w", user.ID, err)
196
+ }
197
+ }(u)
198
+ }
199
+
200
+ wg.Wait()
201
+ close(errs)
202
+
203
+ // Collect first error (or use errgroup for cleaner multi-error handling)
204
+ for err := range errs {
205
+ if err != nil {
206
+ return err
207
+ }
208
+ }
209
+ return nil
210
+ }
211
+ ```
212
+
213
+ ### DO NOT
214
+
215
+ ```go
216
+ // WRONG: wg.Add inside goroutine — race condition
217
+ for _, u := range users {
218
+ go func(user *User) {
219
+ wg.Add(1) // WRONG: may race with wg.Wait()
220
+ defer wg.Done()
221
+ enrich(ctx, user)
222
+ }(u)
223
+ }
224
+ wg.Wait()
225
+ ```
226
+
227
+ ---
228
+
229
+ ## Rule 5 — `errgroup` for concurrent tasks with error collection
230
+
231
+ Prefer `golang.org/x/sync/errgroup` over manual WaitGroup + error channel
232
+ for fan-out where you care about errors.
233
+
234
+ ```go
235
+ import "golang.org/x/sync/errgroup"
236
+
237
+ func (uc *UserUsecase) CreateWithAudit(ctx context.Context, input CreateUserInput) (*User, error) {
238
+ g, ctx := errgroup.WithContext(ctx)
239
+
240
+ var user *User
241
+ g.Go(func() error {
242
+ var err error
243
+ user, err = uc.repo.Create(ctx, &User{Email: input.Email})
244
+ return err
245
+ })
246
+
247
+ g.Go(func() error {
248
+ return uc.auditLog.Record(ctx, "user.create", input.Email)
249
+ })
250
+
251
+ if err := g.Wait(); err != nil {
252
+ return nil, fmt.Errorf("create with audit: %w", err)
253
+ }
254
+ return user, nil
255
+ }
256
+ ```
257
+
258
+ ---
259
+
260
+ ## Rule 6 — Protecting shared state
261
+
262
+ Prefer communicating over sharing. When shared state is necessary, protect
263
+ it with `sync.Mutex`. Use `sync.RWMutex` when reads are far more frequent
264
+ than writes.
265
+
266
+ ```go
267
+ // Cache with RWMutex — many concurrent readers, occasional writer
268
+ type UserCache struct {
269
+ mu sync.RWMutex
270
+ store map[string]*domain.User
271
+ }
272
+
273
+ func (c *UserCache) Get(id string) (*domain.User, bool) {
274
+ c.mu.RLock()
275
+ defer c.mu.RUnlock()
276
+ u, ok := c.store[id]
277
+ return u, ok
278
+ }
279
+
280
+ func (c *UserCache) Set(id string, u *domain.User) {
281
+ c.mu.Lock()
282
+ defer c.mu.Unlock()
283
+ c.store[id] = u
284
+ }
285
+ ```
286
+
287
+ ### DO NOT
288
+
289
+ ```go
290
+ // WRONG: unprotected map access from multiple goroutines — data race
291
+ type BadCache struct {
292
+ store map[string]*User // WRONG: no mutex
293
+ }
294
+
295
+ func (c *BadCache) Get(id string) *User { return c.store[id] } // WRONG
296
+ func (c *BadCache) Set(id string, u *User) { c.store[id] = u } // WRONG
297
+
298
+ // WRONG: holding lock across I/O
299
+ func (c *UserCache) Fetch(ctx context.Context, id string) (*User, error) {
300
+ c.mu.Lock()
301
+ defer c.mu.Unlock()
302
+ // WRONG: lock held while doing a network call — blocks all other readers
303
+ return c.client.Get(ctx, id)
304
+ }
305
+ ```
306
+
307
+ ---
308
+
309
+ ## Rule 7 — Never pass a loop variable to a goroutine by closure in Go < 1.22
310
+
311
+ In Go 1.22+ the loop variable is per-iteration and this is safe. For Go
312
+ 1.21 and earlier, always capture loop variables explicitly.
313
+
314
+ ```go
315
+ // Safe in Go 1.22+ (this project uses Go 1.23 — but be explicit for clarity)
316
+ for _, u := range users {
317
+ u := u // explicit capture — safe in all versions, documents intent
318
+ go func() {
319
+ process(u)
320
+ }()
321
+ }
322
+ ```
323
+
324
+ ---
325
+
326
+ ## Quick Reference: Java → Go Concurrency Mapping
327
+
328
+ | Java | Go |
329
+ |------|----|
330
+ | `ExecutorService` / `ThreadPoolExecutor` | `errgroup` or goroutines + WaitGroup |
331
+ | `CompletableFuture` | goroutine + channel or `errgroup` |
332
+ | `synchronized` method | `sync.Mutex` wrapping the method body |
333
+ | `ConcurrentHashMap` | `sync.Map` or `map` + `sync.RWMutex` |
334
+ | `Thread.sleep()` in tests | channel receive with timeout or `time.After` |
335
+ | `InterruptedException` | `ctx.Done()` / `context.Canceled` |
336
+ | `@Async` Spring annotation | `go func() { ... }()` with context propagation |
@@ -0,0 +1,267 @@
1
+ # Config Management Standard
2
+
3
+ All configuration comes from environment variables. This is the 12-factor
4
+ app method and is mandatory for every service in this fleet.
5
+
6
+ ---
7
+
8
+ ## Rule 1 — Environment variables only; no config files at runtime
9
+
10
+ No YAML, TOML, or JSON config files read at runtime. No `application.properties`.
11
+ Environment variables are injected by the container orchestrator (Kubernetes
12
+ ConfigMaps and Secrets) and are the single source of truth at runtime.
13
+
14
+ ### Java equivalent
15
+ Spring Boot's `application.properties` / `@Value` / `@ConfigurationProperties`
16
+ → Go struct with env tags, loaded once in `main`.
17
+
18
+ ---
19
+
20
+ ## Config Struct with env Tags
21
+
22
+ Use `github.com/caarlos0/env/v11` (or the team-agreed env loader). Define
23
+ one `Config` struct per service. Tag every field.
24
+
25
+ ```go
26
+ // config/config.go
27
+ package config
28
+
29
+ import (
30
+ "fmt"
31
+
32
+ "github.com/caarlos0/env/v11"
33
+ )
34
+
35
+ // Config holds all configuration for this service.
36
+ // Fields tagged `required` cause Load() to return an error if absent.
37
+ // Fields with `envDefault` use that value when the variable is unset.
38
+ type Config struct {
39
+ // Server
40
+ Port string `env:"PORT" envDefault:"8080"`
41
+ ShutdownTimeout int `env:"SHUTDOWN_TIMEOUT" envDefault:"10"` // seconds
42
+
43
+ // Database
44
+ DBHost string `env:"DB_HOST" required:"true"`
45
+ DBPort string `env:"DB_PORT" envDefault:"5432"`
46
+ DBName string `env:"DB_NAME" required:"true"`
47
+ DBUser string `env:"DB_USER" required:"true"`
48
+ DBPassword string `env:"DB_PASSWORD" required:"true"`
49
+ DBSSLMode string `env:"DB_SSLMODE" envDefault:"require"`
50
+
51
+ // Observability
52
+ LogLevel string `env:"LOG_LEVEL" envDefault:"info"`
53
+ OTELEndpoint string `env:"OTEL_ENDPOINT" envDefault:"http://otel-collector:4318"`
54
+ ServiceName string `env:"SERVICE_NAME" required:"true"`
55
+ ServiceVersion string `env:"SERVICE_VERSION" envDefault:"dev"`
56
+
57
+ // Auth
58
+ JWTSecret string `env:"JWT_SECRET" required:"true"`
59
+
60
+ // External services (example)
61
+ NotificationServiceURL string `env:"NOTIFICATION_SERVICE_URL" required:"true"`
62
+ }
63
+
64
+ // Load parses environment variables into Config.
65
+ // Returns an error if any required variable is missing or has an invalid type.
66
+ func Load() (*Config, error) {
67
+ cfg := &Config{}
68
+ if err := env.Parse(cfg); err != nil {
69
+ return nil, fmt.Errorf("loading config: %w", err)
70
+ }
71
+ return cfg, nil
72
+ }
73
+
74
+ // DSN builds the postgres connection string from individual fields.
75
+ // Never expose this method's output in logs.
76
+ func (c *Config) DSN() string {
77
+ return fmt.Sprintf(
78
+ "host=%s port=%s dbname=%s user=%s password=%s sslmode=%s",
79
+ c.DBHost, c.DBPort, c.DBName, c.DBUser, c.DBPassword, c.DBSSLMode,
80
+ )
81
+ }
82
+ ```
83
+
84
+ ---
85
+
86
+ ## Rule 2 — Load once at startup; pass as struct
87
+
88
+ Call `config.Load()` exactly once in `main.go`. Pass the populated struct
89
+ (or specific fields) to constructors. Never call `os.Getenv` anywhere else
90
+ in the codebase.
91
+
92
+ ### DO
93
+
94
+ ```go
95
+ // cmd/server/main.go
96
+ cfg, err := config.Load()
97
+ if err != nil {
98
+ log.Fatalf("loading config: %v", err)
99
+ }
100
+
101
+ // Pass the config struct — or specific fields — to each constructor
102
+ db, err := repository.OpenDB(cfg.DSN())
103
+ if err != nil {
104
+ log.Fatalf("opening db: %v", err)
105
+ }
106
+
107
+ notifClient := notification.NewClient(cfg.NotificationServiceURL)
108
+ ```
109
+
110
+ ### DO NOT
111
+
112
+ ```go
113
+ // WRONG: reading env vars scattered across the codebase
114
+ func NewUserRepository(db *sql.DB) *UserRepository {
115
+ timeout, _ := strconv.Atoi(os.Getenv("DB_TIMEOUT")) // WRONG
116
+ return &UserRepository{db: db, timeout: timeout}
117
+ }
118
+
119
+ // WRONG: global config variable
120
+ var GlobalConfig *config.Config // WRONG: hidden dependency, untestable
121
+
122
+ // WRONG: reading config in a handler
123
+ func (h *UserHandler) Create(w http.ResponseWriter, r *http.Request) {
124
+ maxUsers, _ := strconv.Atoi(os.Getenv("MAX_USERS")) // WRONG
125
+ ...
126
+ }
127
+ ```
128
+
129
+ ---
130
+
131
+ ## Rule 3 — Secrets: never commit, never log
132
+
133
+ Secrets are values in `DBPassword`, `JWTSecret`, API keys, etc.
134
+
135
+ Rules:
136
+ 1. Never commit real secret values to git — `.env` is in `.gitignore`
137
+ 2. Never log a secret or a struct that contains one
138
+ 3. Never include secrets in error messages returned to clients
139
+ 4. Implement `fmt.Stringer` with redaction if the Config struct is ever logged
140
+
141
+ ```go
142
+ // config/config.go — safe String() prevents accidental logging of secrets
143
+ func (c *Config) String() string {
144
+ return fmt.Sprintf(
145
+ "Config{Port:%s, DBHost:%s, DBName:%s, LogLevel:%s, ServiceName:%s}",
146
+ c.Port, c.DBHost, c.DBName, c.LogLevel, c.ServiceName,
147
+ // NOTE: DBPassword, JWTSecret intentionally omitted
148
+ )
149
+ }
150
+ ```
151
+
152
+ ```go
153
+ // DO: log the safe representation
154
+ logger.Info("service starting", zap.String("config", cfg.String()))
155
+
156
+ // DO NOT: log the raw struct
157
+ logger.Info("service starting", zap.Any("config", cfg)) // WRONG: leaks secrets
158
+ ```
159
+
160
+ ---
161
+
162
+ ## `.env.example` — Documents all variables
163
+
164
+ Every service ships a `.env.example` that documents every variable with a
165
+ description but uses only placeholder values. This is committed to git and
166
+ is the primary operator documentation for deployment.
167
+
168
+ ```bash
169
+ # .env.example
170
+ # Copy to .env for local development. Never commit .env.
171
+
172
+ # --- Server ---
173
+ PORT=8080
174
+ SHUTDOWN_TIMEOUT=10 # seconds to wait for in-flight requests on SIGTERM
175
+
176
+ # --- Database ---
177
+ DB_HOST=localhost # required — PostgreSQL host
178
+ DB_PORT=5432
179
+ DB_NAME=myservice_db # required
180
+ DB_USER=myservice # required
181
+ DB_PASSWORD=CHANGEME # required — use a strong random value in production
182
+ DB_SSLMODE=disable # use 'require' in production
183
+
184
+ # --- Observability ---
185
+ LOG_LEVEL=info # debug | info | warn | error
186
+ OTEL_ENDPOINT=http://otel-collector:4318
187
+ SERVICE_NAME=my-service # required — used as Prometheus/Loki label
188
+ SERVICE_VERSION=dev
189
+
190
+ # --- Auth ---
191
+ JWT_SECRET=CHANGEME # required — minimum 32 bytes of entropy in production
192
+
193
+ # --- External services ---
194
+ NOTIFICATION_SERVICE_URL=http://notification-svc:8080 # required
195
+ ```
196
+
197
+ `.gitignore` entry (must be present):
198
+
199
+ ```
200
+ .env
201
+ *.env.local
202
+ ```
203
+
204
+ ---
205
+
206
+ ## Rule 4 — Type-safe default handling
207
+
208
+ Numeric and boolean env vars must be declared with the correct Go type in
209
+ the Config struct so the env loader validates them automatically.
210
+
211
+ ```go
212
+ type Config struct {
213
+ Port int `env:"PORT" envDefault:"8080"`
214
+ ShutdownTimeout time.Duration `env:"SHUTDOWN_TIMEOUT" envDefault:"10s"`
215
+ EnableProfiling bool `env:"ENABLE_PROFILING" envDefault:"false"`
216
+ MaxDBConns int `env:"MAX_DB_CONNS" envDefault:"25"`
217
+ }
218
+ ```
219
+
220
+ ### DO NOT
221
+
222
+ ```go
223
+ // WRONG: everything as string, manual conversion at use site
224
+ type Config struct {
225
+ Port string `env:"PORT"` // WRONG: forces strconv everywhere
226
+ }
227
+
228
+ port, _ := strconv.Atoi(cfg.Port) // WRONG: error ignored, repeated across codebase
229
+ ```
230
+
231
+ ---
232
+
233
+ ## Validation Beyond Required Tags
234
+
235
+ For cross-field validation or range checks, validate in `Load()` after
236
+ parsing:
237
+
238
+ ```go
239
+ func Load() (*Config, error) {
240
+ cfg := &Config{}
241
+ if err := env.Parse(cfg); err != nil {
242
+ return nil, fmt.Errorf("loading config: %w", err)
243
+ }
244
+
245
+ if cfg.MaxDBConns < 1 || cfg.MaxDBConns > 100 {
246
+ return nil, fmt.Errorf("MAX_DB_CONNS must be between 1 and 100, got %d", cfg.MaxDBConns)
247
+ }
248
+ if len(cfg.JWTSecret) < 32 {
249
+ return nil, fmt.Errorf("JWT_SECRET must be at least 32 characters")
250
+ }
251
+
252
+ return cfg, nil
253
+ }
254
+ ```
255
+
256
+ ---
257
+
258
+ ## Quick Reference: Java → Go Config Mapping
259
+
260
+ | Spring Boot | Go |
261
+ |-------------|----|
262
+ | `application.properties` | environment variables |
263
+ | `@Value("${server.port}")` | `env:"PORT"` tag on struct field |
264
+ | `@ConfigurationProperties(prefix="db")` | nested struct or flat `Config` struct |
265
+ | `spring.datasource.url` | `DB_HOST`, `DB_PORT`, `DB_NAME` separate vars |
266
+ | Vault / AWS Secrets Manager | Env vars injected by K8s Secrets (same end result) |
267
+ | Profile-based config (`dev`, `prod`) | Env vars differ per deployment; no profile concept |