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.
- package/dist/config-patcher.d.ts +20 -50
- package/dist/config-patcher.d.ts.map +1 -1
- package/dist/config-patcher.js +138 -102
- package/dist/config-patcher.js.map +1 -1
- package/dist/index.js +75 -22
- package/dist/index.js.map +1 -1
- package/dist/installer.d.ts +0 -18
- package/dist/installer.d.ts.map +1 -1
- package/dist/installer.js +19 -39
- package/dist/installer.js.map +1 -1
- package/dist/types.d.ts +10 -6
- package/dist/types.d.ts.map +1 -1
- package/dist/uninstaller.d.ts +0 -23
- package/dist/uninstaller.d.ts.map +1 -1
- package/dist/uninstaller.js +22 -68
- package/dist/uninstaller.js.map +1 -1
- package/package.json +3 -2
- package/plugins/go-reviewer/.claude-plugin/plugin.json +12 -0
- package/plugins/go-reviewer/commands/go-review.md +424 -0
- package/plugins/go-reviewer/mcp-servers/go-reviewer-mcp/README.md +236 -0
- package/plugins/go-reviewer/mcp-servers/go-reviewer-mcp/main.go +678 -0
- package/plugins/go-scaffolder/.claude-plugin/plugin.json +12 -0
- package/plugins/go-scaffolder/commands/scaffold-service.md +802 -0
- package/plugins/go-scaffolder/reference-service/.env.example +27 -0
- package/plugins/go-scaffolder/reference-service/Dockerfile +55 -0
- package/plugins/go-scaffolder/reference-service/REFERENCE-SERVICE-NOTICE.md +104 -0
- package/plugins/go-scaffolder/reference-service/cmd/server/main.go +266 -0
- package/plugins/go-scaffolder/reference-service/config/config.go +67 -0
- package/plugins/go-scaffolder/reference-service/go.mod +17 -0
- package/plugins/go-scaffolder/reference-service/internal/domain/booking.go +118 -0
- package/plugins/go-scaffolder/reference-service/internal/handler/booking.go +242 -0
- package/plugins/go-scaffolder/reference-service/internal/handler/booking_test.go +451 -0
- package/plugins/go-scaffolder/reference-service/internal/repository/booking_postgres.go +124 -0
- package/plugins/go-scaffolder/reference-service/internal/usecase/booking.go +181 -0
- package/plugins/go-standards/.claude-plugin/plugin.json +22 -0
- package/plugins/go-standards/commands/go-standards-check.md +232 -0
- package/plugins/go-standards/skills/concurrency.md +336 -0
- package/plugins/go-standards/skills/config.md +267 -0
- package/plugins/go-standards/skills/error-handling.md +286 -0
- package/plugins/go-standards/skills/http-chi.md +390 -0
- package/plugins/go-standards/skills/logging-observability.md +340 -0
- package/plugins/go-standards/skills/naming-and-style.md +315 -0
- package/plugins/go-standards/skills/project-layout.md +313 -0
- package/plugins/go-standards/skills/testing.md +366 -0
- package/plugins/java2go-porter/.claude-plugin/plugin.json +21 -0
- package/plugins/java2go-porter/agents/analyzer.md +232 -0
- package/plugins/java2go-porter/agents/reviewer.md +241 -0
- package/plugins/java2go-porter/agents/test-pairer.md +365 -0
- package/plugins/java2go-porter/agents/translator.md +419 -0
- package/plugins/java2go-porter/commands/port-java-service.md +149 -0
- package/plugins/java2go-porter/skills/idiom-mapping.md +75 -0
- package/plugins/migration-safety/.claude-plugin/plugin.json +20 -0
- package/plugins/migration-safety/commands/gen-characterization-test.md +452 -0
- package/plugins/migration-safety/commands/strangler-plan.md +356 -0
- package/plugins/migration-safety/skills/strangler-fig.md +382 -0
|
@@ -0,0 +1,340 @@
|
|
|
1
|
+
# Logging and Observability Standard
|
|
2
|
+
|
|
3
|
+
The observability stack is Grafana + Prometheus + Loki + Tempo. Every
|
|
4
|
+
service must emit structured logs (JSON → Loki), Prometheus metrics, and
|
|
5
|
+
OpenTelemetry traces (→ Tempo) with trace IDs correlated across all three.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Structured Logging with zap
|
|
10
|
+
|
|
11
|
+
Use `go.uber.org/zap` in production mode. JSON output is required for Loki
|
|
12
|
+
ingestion. Never use `fmt.Println`, `log.Println`, or `log.Printf` in
|
|
13
|
+
production code paths.
|
|
14
|
+
|
|
15
|
+
### Logger initialization
|
|
16
|
+
|
|
17
|
+
```go
|
|
18
|
+
// cmd/server/main.go
|
|
19
|
+
import "go.uber.org/zap"
|
|
20
|
+
|
|
21
|
+
func buildLogger(level, serviceName, serviceVersion string) (*zap.Logger, error) {
|
|
22
|
+
cfg := zap.NewProductionConfig()
|
|
23
|
+
|
|
24
|
+
// Parse level
|
|
25
|
+
var zapLevel zap.AtomicLevel
|
|
26
|
+
if err := zapLevel.UnmarshalText([]byte(level)); err != nil {
|
|
27
|
+
return nil, fmt.Errorf("invalid log level %q: %w", level, err)
|
|
28
|
+
}
|
|
29
|
+
cfg.Level = zapLevel
|
|
30
|
+
|
|
31
|
+
// Always add service-level fields for Loki label extraction
|
|
32
|
+
cfg.InitialFields = map[string]interface{}{
|
|
33
|
+
"service": serviceName,
|
|
34
|
+
"version": serviceVersion,
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
logger, err := cfg.Build()
|
|
38
|
+
if err != nil {
|
|
39
|
+
return nil, fmt.Errorf("building logger: %w", err)
|
|
40
|
+
}
|
|
41
|
+
return logger, nil
|
|
42
|
+
}
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
### Passing the logger
|
|
46
|
+
|
|
47
|
+
Pass `*zap.Logger` via constructor — never use a global logger variable.
|
|
48
|
+
|
|
49
|
+
```go
|
|
50
|
+
repo := repository.NewUserRepository(db, logger)
|
|
51
|
+
uc := usecase.NewUserUsecase(repo, logger)
|
|
52
|
+
h := handler.NewUserHandler(uc, logger)
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
---
|
|
56
|
+
|
|
57
|
+
## Log Levels
|
|
58
|
+
|
|
59
|
+
| Level | When to use |
|
|
60
|
+
|-------|-------------|
|
|
61
|
+
| `Error` | Something went wrong that requires operator attention. Always include `zap.Error(err)`. |
|
|
62
|
+
| `Warn` | Degraded state — the service is working but something is off (retries, fallback used). |
|
|
63
|
+
| `Info` | Business-significant events (user created, order placed, payment processed). |
|
|
64
|
+
| `Debug` | Developer-oriented detail (SQL queries, cache hits). Disabled in production. |
|
|
65
|
+
|
|
66
|
+
### DO
|
|
67
|
+
|
|
68
|
+
```go
|
|
69
|
+
// Error — actionable failure with structured context
|
|
70
|
+
logger.Error("create user failed",
|
|
71
|
+
zap.Error(err),
|
|
72
|
+
zap.String("email", input.Email),
|
|
73
|
+
zap.String("request_id", requestID),
|
|
74
|
+
zap.String("trace_id", traceID),
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
// Info — business event, structured fields
|
|
78
|
+
logger.Info("user created",
|
|
79
|
+
zap.String("user_id", user.ID),
|
|
80
|
+
zap.String("email", user.Email),
|
|
81
|
+
zap.Duration("duration", time.Since(start)),
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
// Warn — degraded but continuing
|
|
85
|
+
logger.Warn("cache miss, falling back to db",
|
|
86
|
+
zap.String("key", cacheKey),
|
|
87
|
+
zap.Duration("cache_latency", latency),
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
// Debug — dev only, never in prod
|
|
91
|
+
logger.Debug("executing sql",
|
|
92
|
+
zap.String("query", query),
|
|
93
|
+
zap.Any("args", args),
|
|
94
|
+
)
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
### DO NOT
|
|
98
|
+
|
|
99
|
+
```go
|
|
100
|
+
fmt.Println("user created: " + user.ID) // WRONG: unstructured, lost in Loki
|
|
101
|
+
log.Printf("error: %v", err) // WRONG: unstructured stdlib log
|
|
102
|
+
logger.Info(fmt.Sprintf("user %s created", id)) // WRONG: interpolated string not queryable
|
|
103
|
+
logger.Error("something went wrong") // WRONG: no zap.Error(err), not actionable
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
---
|
|
107
|
+
|
|
108
|
+
## Trace ID Correlation
|
|
109
|
+
|
|
110
|
+
Every log entry for a request must include the trace ID so Grafana can link
|
|
111
|
+
Loki logs to a Tempo trace. Extract the trace ID from the span and inject it
|
|
112
|
+
into a child logger at the handler level.
|
|
113
|
+
|
|
114
|
+
```go
|
|
115
|
+
// internal/handler/user_handler.go
|
|
116
|
+
import (
|
|
117
|
+
"go.opentelemetry.io/otel/trace"
|
|
118
|
+
"go.uber.org/zap"
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
func (h *UserHandler) Create(w http.ResponseWriter, r *http.Request) {
|
|
122
|
+
ctx := r.Context()
|
|
123
|
+
span := trace.SpanFromContext(ctx)
|
|
124
|
+
traceID := span.SpanContext().TraceID().String()
|
|
125
|
+
|
|
126
|
+
// Create a request-scoped logger with trace_id baked in
|
|
127
|
+
log := h.logger.With(
|
|
128
|
+
zap.String("trace_id", traceID),
|
|
129
|
+
zap.String("request_id", middleware.GetReqID(ctx)),
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
user, err := h.uc.Create(ctx, input)
|
|
133
|
+
if err != nil {
|
|
134
|
+
log.Error("create user", zap.Error(err))
|
|
135
|
+
respondError(w, err)
|
|
136
|
+
return
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
log.Info("user created", zap.String("user_id", user.ID))
|
|
140
|
+
respondJSON(w, http.StatusCreated, user)
|
|
141
|
+
}
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
---
|
|
145
|
+
|
|
146
|
+
## Prometheus Metrics
|
|
147
|
+
|
|
148
|
+
Register metrics at package init. Use the naming convention
|
|
149
|
+
`<service>_<subsystem>_<name>_<unit>`.
|
|
150
|
+
|
|
151
|
+
```go
|
|
152
|
+
// internal/metrics/metrics.go
|
|
153
|
+
package metrics
|
|
154
|
+
|
|
155
|
+
import (
|
|
156
|
+
"github.com/prometheus/client_golang/prometheus"
|
|
157
|
+
"github.com/prometheus/client_golang/prometheus/promauto"
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
var (
|
|
161
|
+
HTTPRequestsTotal = promauto.NewCounterVec(
|
|
162
|
+
prometheus.CounterOpts{
|
|
163
|
+
Name: "user_service_http_requests_total",
|
|
164
|
+
Help: "Total number of HTTP requests by method, path, and status.",
|
|
165
|
+
},
|
|
166
|
+
[]string{"method", "path", "status"},
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
HTTPRequestDuration = promauto.NewHistogramVec(
|
|
170
|
+
prometheus.HistogramOpts{
|
|
171
|
+
Name: "user_service_http_duration_seconds",
|
|
172
|
+
Help: "HTTP request duration in seconds.",
|
|
173
|
+
Buckets: prometheus.DefBuckets, // .005 .01 .025 .05 .1 .25 .5 1 2.5 5 10
|
|
174
|
+
},
|
|
175
|
+
[]string{"method", "path"},
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
ActiveConnections = promauto.NewGauge(prometheus.GaugeOpts{
|
|
179
|
+
Name: "user_service_active_connections",
|
|
180
|
+
Help: "Number of active HTTP connections.",
|
|
181
|
+
})
|
|
182
|
+
|
|
183
|
+
DBQueryDuration = promauto.NewHistogramVec(
|
|
184
|
+
prometheus.HistogramOpts{
|
|
185
|
+
Name: "user_service_db_query_duration_seconds",
|
|
186
|
+
Help: "Database query duration in seconds.",
|
|
187
|
+
Buckets: []float64{.001, .005, .01, .025, .05, .1, .5, 1},
|
|
188
|
+
},
|
|
189
|
+
[]string{"operation", "table"},
|
|
190
|
+
)
|
|
191
|
+
)
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
### Metrics middleware
|
|
195
|
+
|
|
196
|
+
```go
|
|
197
|
+
// internal/handler/middleware/metrics.go
|
|
198
|
+
package middleware
|
|
199
|
+
|
|
200
|
+
import (
|
|
201
|
+
"net/http"
|
|
202
|
+
"strconv"
|
|
203
|
+
"time"
|
|
204
|
+
|
|
205
|
+
"github.com/go-chi/chi/v5"
|
|
206
|
+
chimiddleware "github.com/go-chi/chi/v5/middleware"
|
|
207
|
+
|
|
208
|
+
"github.com/zokypesch/go-ga-lib/internal/metrics"
|
|
209
|
+
)
|
|
210
|
+
|
|
211
|
+
func PrometheusMetrics(next http.Handler) http.Handler {
|
|
212
|
+
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
213
|
+
start := time.Now()
|
|
214
|
+
ww := chimiddleware.NewWrapResponseWriter(w, r.ProtoMajor)
|
|
215
|
+
|
|
216
|
+
next.ServeHTTP(ww, r)
|
|
217
|
+
|
|
218
|
+
routePattern := chi.RouteContext(r.Context()).RoutePattern()
|
|
219
|
+
duration := time.Since(start).Seconds()
|
|
220
|
+
status := strconv.Itoa(ww.Status())
|
|
221
|
+
|
|
222
|
+
metrics.HTTPRequestsTotal.WithLabelValues(r.Method, routePattern, status).Inc()
|
|
223
|
+
metrics.HTTPRequestDuration.WithLabelValues(r.Method, routePattern).Observe(duration)
|
|
224
|
+
})
|
|
225
|
+
}
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
---
|
|
229
|
+
|
|
230
|
+
## OpenTelemetry Tracing (→ Tempo)
|
|
231
|
+
|
|
232
|
+
```go
|
|
233
|
+
// config/otel.go
|
|
234
|
+
package config
|
|
235
|
+
|
|
236
|
+
import (
|
|
237
|
+
"context"
|
|
238
|
+
"fmt"
|
|
239
|
+
|
|
240
|
+
"go.opentelemetry.io/otel"
|
|
241
|
+
"go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp"
|
|
242
|
+
"go.opentelemetry.io/otel/propagation"
|
|
243
|
+
"go.opentelemetry.io/otel/sdk/resource"
|
|
244
|
+
sdktrace "go.opentelemetry.io/otel/sdk/trace"
|
|
245
|
+
semconv "go.opentelemetry.io/otel/semconv/v1.26.0"
|
|
246
|
+
)
|
|
247
|
+
|
|
248
|
+
func InitTracer(ctx context.Context, endpoint, serviceName, serviceVersion string) (func(context.Context) error, error) {
|
|
249
|
+
exporter, err := otlptracehttp.New(ctx,
|
|
250
|
+
otlptracehttp.WithEndpoint(endpoint),
|
|
251
|
+
otlptracehttp.WithInsecure(),
|
|
252
|
+
)
|
|
253
|
+
if err != nil {
|
|
254
|
+
return nil, fmt.Errorf("creating OTLP exporter: %w", err)
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
res, err := resource.New(ctx,
|
|
258
|
+
resource.WithAttributes(
|
|
259
|
+
semconv.ServiceName(serviceName),
|
|
260
|
+
semconv.ServiceVersion(serviceVersion),
|
|
261
|
+
),
|
|
262
|
+
)
|
|
263
|
+
if err != nil {
|
|
264
|
+
return nil, fmt.Errorf("creating resource: %w", err)
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
tp := sdktrace.NewTracerProvider(
|
|
268
|
+
sdktrace.WithBatcher(exporter),
|
|
269
|
+
sdktrace.WithResource(res),
|
|
270
|
+
)
|
|
271
|
+
|
|
272
|
+
otel.SetTracerProvider(tp)
|
|
273
|
+
otel.SetTextMapPropagator(propagation.NewCompositeTextMapPropagator(
|
|
274
|
+
propagation.TraceContext{},
|
|
275
|
+
propagation.Baggage{},
|
|
276
|
+
))
|
|
277
|
+
|
|
278
|
+
return tp.Shutdown, nil
|
|
279
|
+
}
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
### Using spans in usecases
|
|
283
|
+
|
|
284
|
+
```go
|
|
285
|
+
// internal/usecase/user_usecase.go
|
|
286
|
+
import "go.opentelemetry.io/otel"
|
|
287
|
+
|
|
288
|
+
var tracer = otel.Tracer("user-service/usecase")
|
|
289
|
+
|
|
290
|
+
func (uc *UserUsecase) Create(ctx context.Context, input domain.CreateUserInput) (*domain.User, error) {
|
|
291
|
+
ctx, span := tracer.Start(ctx, "UserUsecase.Create")
|
|
292
|
+
defer span.End()
|
|
293
|
+
|
|
294
|
+
span.SetAttributes(
|
|
295
|
+
attribute.String("user.email", input.Email),
|
|
296
|
+
)
|
|
297
|
+
|
|
298
|
+
user, err := uc.repo.Create(ctx, &domain.User{Email: input.Email})
|
|
299
|
+
if err != nil {
|
|
300
|
+
span.RecordError(err)
|
|
301
|
+
span.SetStatus(codes.Error, err.Error())
|
|
302
|
+
return nil, fmt.Errorf("create user: %w", err)
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
span.SetAttributes(attribute.String("user.id", user.ID))
|
|
306
|
+
return user, nil
|
|
307
|
+
}
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
---
|
|
311
|
+
|
|
312
|
+
## Standard Metric Naming
|
|
313
|
+
|
|
314
|
+
```
|
|
315
|
+
<service_name>_http_requests_total counter {method, path, status}
|
|
316
|
+
<service_name>_http_duration_seconds histogram {method, path}
|
|
317
|
+
<service_name>_active_connections gauge
|
|
318
|
+
<service_name>_db_query_duration_seconds histogram {operation, table}
|
|
319
|
+
<service_name>_cache_hits_total counter {cache}
|
|
320
|
+
<service_name>_cache_misses_total counter {cache}
|
|
321
|
+
<service_name>_external_calls_total counter {service, operation, status}
|
|
322
|
+
<service_name>_external_call_duration_seconds histogram {service, operation}
|
|
323
|
+
```
|
|
324
|
+
|
|
325
|
+
---
|
|
326
|
+
|
|
327
|
+
## DO NOT
|
|
328
|
+
|
|
329
|
+
```go
|
|
330
|
+
fmt.Println("user created") // WRONG: unstructured
|
|
331
|
+
log.Println("error:", err) // WRONG: stdlib log
|
|
332
|
+
logger.Info("user " + id + " created") // WRONG: interpolated, not queryable
|
|
333
|
+
logger.Error("failed") // WRONG: no zap.Error(err)
|
|
334
|
+
prometheus.MustRegister(counter) // WRONG in tests — use promauto in prod
|
|
335
|
+
// WRONG: registering metrics inside a function called per request
|
|
336
|
+
func (h *Handler) Create(...) {
|
|
337
|
+
counter := prometheus.NewCounter(...) // WRONG: re-registers every call, panics
|
|
338
|
+
prometheus.MustRegister(counter)
|
|
339
|
+
}
|
|
340
|
+
```
|
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
# Naming and Style Standard
|
|
2
|
+
|
|
3
|
+
Go naming conventions differ fundamentally from Java. This skill maps the
|
|
4
|
+
Java patterns you know to their idiomatic Go equivalents, and calls out the
|
|
5
|
+
anti-patterns that must not appear in migrated code.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Package Names
|
|
10
|
+
|
|
11
|
+
Packages are lowercase, single words, no underscores, no plurals.
|
|
12
|
+
The package name IS the namespace — it will prefix every exported name at
|
|
13
|
+
the call site, so make the combination read naturally.
|
|
14
|
+
|
|
15
|
+
### DO
|
|
16
|
+
|
|
17
|
+
```
|
|
18
|
+
handler not handlerService, not handlers
|
|
19
|
+
usecase not useCaseService, not use_case
|
|
20
|
+
repository not repositoryLayer, not repo_layer (repo is acceptable as abbreviation)
|
|
21
|
+
domain not models, not entities, not dto
|
|
22
|
+
config not configuration, not conf_loader
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
```go
|
|
26
|
+
// Reads naturally: handler.NewUserHandler, usecase.NewUserUsecase
|
|
27
|
+
package handler
|
|
28
|
+
package usecase
|
|
29
|
+
package repository
|
|
30
|
+
package domain
|
|
31
|
+
package config
|
|
32
|
+
package metrics
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
### DO NOT
|
|
36
|
+
|
|
37
|
+
```go
|
|
38
|
+
package handlerService // WRONG: redundant suffix
|
|
39
|
+
package userHandlers // WRONG: plural and redundant
|
|
40
|
+
package util // WRONG: meaningless catch-all
|
|
41
|
+
package common // WRONG: a dumping ground
|
|
42
|
+
package helpers // WRONG: Java holdover
|
|
43
|
+
package base // WRONG: no abstract base classes in Go
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
---
|
|
47
|
+
|
|
48
|
+
## Exported Type Names
|
|
49
|
+
|
|
50
|
+
Types are nouns. Service types include their role only when it disambiguates.
|
|
51
|
+
|
|
52
|
+
### DO
|
|
53
|
+
|
|
54
|
+
```go
|
|
55
|
+
type User struct { ... }
|
|
56
|
+
type Order struct { ... }
|
|
57
|
+
type UserHandler struct { ... } // Handler suffix is the role — ok
|
|
58
|
+
type UserUsecase struct { ... } // Usecase suffix distinguishes from domain.User — ok
|
|
59
|
+
type UserRepository struct { ... } // Repository suffix distinguishes from domain.User — ok
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
### DO NOT
|
|
63
|
+
|
|
64
|
+
```go
|
|
65
|
+
type UserManager struct { ... } // WRONG: Manager = vague, Java-ism
|
|
66
|
+
type UserHelper struct { ... } // WRONG: Helper = vague
|
|
67
|
+
type UserUtil struct { ... } // WRONG: Util = vague
|
|
68
|
+
type AbstractUser struct { ... } // WRONG: no abstract classes
|
|
69
|
+
type BaseHandler struct { ... } // WRONG: use composition, not inheritance
|
|
70
|
+
type IUserRepository interface { } // WRONG: I prefix is C#/Java; Go uses plain names
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
---
|
|
74
|
+
|
|
75
|
+
## Interface Naming
|
|
76
|
+
|
|
77
|
+
Single-method interfaces: method name + `er`.
|
|
78
|
+
Multi-method interfaces: describe the capability as a noun phrase.
|
|
79
|
+
|
|
80
|
+
### DO
|
|
81
|
+
|
|
82
|
+
```go
|
|
83
|
+
type Storer interface {
|
|
84
|
+
Store(ctx context.Context, key string, value []byte) error
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
type Reader interface {
|
|
88
|
+
Read(ctx context.Context, key string) ([]byte, error)
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
type ReadWriter interface {
|
|
92
|
+
Reader
|
|
93
|
+
Writer
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Domain repository — multi-method, describes the contract
|
|
97
|
+
type UserRepository interface {
|
|
98
|
+
FindByID(ctx context.Context, id string) (*User, error)
|
|
99
|
+
Create(ctx context.Context, u *User) (*User, error)
|
|
100
|
+
Update(ctx context.Context, u *User) error
|
|
101
|
+
Delete(ctx context.Context, id string) error
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Domain usecase — multi-method
|
|
105
|
+
type UserUsecase interface {
|
|
106
|
+
Get(ctx context.Context, id string) (*User, error)
|
|
107
|
+
Create(ctx context.Context, input CreateUserInput) (*User, error)
|
|
108
|
+
}
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
### DO NOT
|
|
112
|
+
|
|
113
|
+
```go
|
|
114
|
+
type IUserRepository interface { ... } // WRONG: I prefix (Java/C#)
|
|
115
|
+
type UserRepositoryInterface interface { ... } // WRONG: Interface suffix
|
|
116
|
+
type AbstractUserService interface { ... } // WRONG: Abstract prefix
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
---
|
|
120
|
+
|
|
121
|
+
## Receiver Names
|
|
122
|
+
|
|
123
|
+
Short, consistent, based on the type name. Never use `self` or `this`.
|
|
124
|
+
|
|
125
|
+
### DO
|
|
126
|
+
|
|
127
|
+
```go
|
|
128
|
+
type UserHandler struct { ... }
|
|
129
|
+
func (h *UserHandler) Create(...) { } // h for Handler
|
|
130
|
+
func (h *UserHandler) Get(...) { } // consistent across all methods
|
|
131
|
+
|
|
132
|
+
type UserUsecase struct { ... }
|
|
133
|
+
func (uc *UserUsecase) Create(...) { } // uc for Usecase
|
|
134
|
+
|
|
135
|
+
type userRepository struct { ... }
|
|
136
|
+
func (r *userRepository) Create(...) { } // r for Repository
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
### DO NOT
|
|
140
|
+
|
|
141
|
+
```go
|
|
142
|
+
func (self *UserHandler) Create(...) { } // WRONG: self is Python
|
|
143
|
+
func (this *UserHandler) Create(...) { } // WRONG: this is Java/C++
|
|
144
|
+
func (userHandler *UserHandler) Create(...) { } // WRONG: too verbose
|
|
145
|
+
func (u *UserHandler) Create(...) { } // WRONG: u is also used for *User — ambiguous
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
---
|
|
149
|
+
|
|
150
|
+
## Constructor Naming
|
|
151
|
+
|
|
152
|
+
Always `NewX`. Returns `*X` and optionally an error if initialization can fail.
|
|
153
|
+
|
|
154
|
+
### DO
|
|
155
|
+
|
|
156
|
+
```go
|
|
157
|
+
// Simple — no error possible
|
|
158
|
+
func NewUserHandler(uc domain.UserUsecase, logger *zap.Logger) *UserHandler {
|
|
159
|
+
return &UserHandler{uc: uc, logger: logger}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Complex — returns error if init can fail
|
|
163
|
+
func NewUserRepository(db *sql.DB) (*userRepository, error) {
|
|
164
|
+
if db == nil {
|
|
165
|
+
return nil, errors.New("db must not be nil")
|
|
166
|
+
}
|
|
167
|
+
if err := db.Ping(); err != nil {
|
|
168
|
+
return nil, fmt.Errorf("pinging db: %w", err)
|
|
169
|
+
}
|
|
170
|
+
return &userRepository{db: db}, nil
|
|
171
|
+
}
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
### DO NOT
|
|
175
|
+
|
|
176
|
+
```go
|
|
177
|
+
func CreateUserHandler(...) *UserHandler { } // WRONG: Create prefix
|
|
178
|
+
func BuildUserHandler(...) *UserHandler { } // WRONG: Build prefix (use Builder pattern only when warranted)
|
|
179
|
+
func GetUserHandler(...) *UserHandler { } // WRONG: Get prefix
|
|
180
|
+
func MakeUserHandler(...) *UserHandler { } // WRONG: Make prefix (reserved for make() built-in)
|
|
181
|
+
func UserHandlerFactory(...) *UserHandler { } // WRONG: Factory suffix
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
---
|
|
185
|
+
|
|
186
|
+
## No Getters and Setters
|
|
187
|
+
|
|
188
|
+
Go structs expose fields directly unless access control or lazy
|
|
189
|
+
initialization requires otherwise. Getters named `GetFoo()` are a Java
|
|
190
|
+
habit; write `Foo()` if an accessor is needed at all.
|
|
191
|
+
|
|
192
|
+
### DO
|
|
193
|
+
|
|
194
|
+
```go
|
|
195
|
+
type User struct {
|
|
196
|
+
ID string
|
|
197
|
+
Email string
|
|
198
|
+
Name string
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// Direct field access — idiomatic
|
|
202
|
+
user.Email = "new@example.com"
|
|
203
|
+
fmt.Println(user.Name)
|
|
204
|
+
|
|
205
|
+
// Method with meaning — not a getter
|
|
206
|
+
func (u *User) DisplayName() string {
|
|
207
|
+
if u.Name != "" {
|
|
208
|
+
return u.Name
|
|
209
|
+
}
|
|
210
|
+
return u.Email
|
|
211
|
+
}
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
### DO NOT
|
|
215
|
+
|
|
216
|
+
```go
|
|
217
|
+
// WRONG: Java-style getters/setters
|
|
218
|
+
func (u *User) GetID() string { return u.ID } // WRONG
|
|
219
|
+
func (u *User) SetID(id string) { u.ID = id } // WRONG
|
|
220
|
+
func (u *User) GetEmail() string { return u.Email } // WRONG
|
|
221
|
+
func (u *User) SetEmail(e string) { u.Email = e } // WRONG
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
Exception: implement the `fmt.Stringer` interface as `String()`, not
|
|
225
|
+
`ToString()`. Implement `json.Marshaler` as `MarshalJSON()`, etc. Standard
|
|
226
|
+
library interface methods are exempt from the no-getter rule.
|
|
227
|
+
|
|
228
|
+
---
|
|
229
|
+
|
|
230
|
+
## No Abstract Base Classes — Use Composition
|
|
231
|
+
|
|
232
|
+
Go has no inheritance. Share behavior via interfaces and embedding, not
|
|
233
|
+
abstract classes.
|
|
234
|
+
|
|
235
|
+
### DO
|
|
236
|
+
|
|
237
|
+
```go
|
|
238
|
+
// Shared logging behavior via embedding
|
|
239
|
+
type baseHandler struct {
|
|
240
|
+
logger *zap.Logger
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
func (b *baseHandler) logError(ctx context.Context, op string, err error) {
|
|
244
|
+
b.logger.Error(op, zap.Error(err), zap.String("request_id", middleware.GetReqID(ctx)))
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
type UserHandler struct {
|
|
248
|
+
baseHandler // embedded — inherits logError
|
|
249
|
+
uc domain.UserUsecase
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
type OrderHandler struct {
|
|
253
|
+
baseHandler
|
|
254
|
+
oc domain.OrderUsecase
|
|
255
|
+
}
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
### DO NOT
|
|
259
|
+
|
|
260
|
+
```go
|
|
261
|
+
// WRONG: Java-style abstract base
|
|
262
|
+
type AbstractHandler struct {
|
|
263
|
+
logger *zap.Logger
|
|
264
|
+
}
|
|
265
|
+
// Extending via struct embedding is fine — naming it Abstract is not
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
---
|
|
269
|
+
|
|
270
|
+
## File Organization
|
|
271
|
+
|
|
272
|
+
One file per major type. Split by responsibility, not by pattern.
|
|
273
|
+
|
|
274
|
+
### DO
|
|
275
|
+
|
|
276
|
+
```
|
|
277
|
+
internal/handler/
|
|
278
|
+
user_handler.go // UserHandler and its methods
|
|
279
|
+
order_handler.go // OrderHandler and its methods
|
|
280
|
+
health_handler.go // HealthHandler
|
|
281
|
+
respond.go // shared response helpers
|
|
282
|
+
decode.go // shared decoding helpers
|
|
283
|
+
middleware/
|
|
284
|
+
auth.go
|
|
285
|
+
metrics.go
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
### DO NOT
|
|
289
|
+
|
|
290
|
+
```
|
|
291
|
+
internal/handler/
|
|
292
|
+
handlers.go // WRONG: all handlers in one file
|
|
293
|
+
interfaces.go // WRONG: don't separate interfaces into their own file
|
|
294
|
+
models.go // WRONG: models belong in domain/
|
|
295
|
+
utils.go // WRONG: no utils files
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
---
|
|
299
|
+
|
|
300
|
+
## Quick Reference: Java Anti-Pattern → Go Idiomatic
|
|
301
|
+
|
|
302
|
+
| Java anti-pattern | Go idiom |
|
|
303
|
+
|-------------------|----------|
|
|
304
|
+
| `UserManager` class | `UserUsecase` or inline in handler |
|
|
305
|
+
| `UserHelper` / `UserUtil` | function in the package that uses it |
|
|
306
|
+
| `AbstractBaseHandler` | embedded struct (unnamed or descriptively named) |
|
|
307
|
+
| `IUserRepository` interface | `UserRepository` interface (no I prefix) |
|
|
308
|
+
| `getUserById()` method | `Get` method or `FindByID` method |
|
|
309
|
+
| `setEmail(String email)` | `user.Email = email` |
|
|
310
|
+
| `UserDTO` / `UserVO` | input/output structs inline: `CreateUserInput`, `UserResponse` |
|
|
311
|
+
| `UserServiceImpl` | `userUsecase` (unexported concrete, exported interface) |
|
|
312
|
+
| `@Override` method | satisfies interface implicitly — no annotation |
|
|
313
|
+
| `final` field | unexported field or struct value semantics |
|
|
314
|
+
| `Optional<User>` | `(*User, error)` return pair |
|
|
315
|
+
| `@Builder` (Lombok) | functional options (`WithTimeout`) or plain struct literal |
|