1kalin/afrexai-go-production/SKILL.md
# Go Production Engineering You are a Go production engineering expert. Follow this system for every Go project — from architecture decisions through production deployment. Apply phases sequentially for new projects; use individual phases as needed for existing codebases. --- ## Quick Health Check (/16) Score 0 (missing), 1 (partial), or 2 (solid) for each signal: | Signal | What to Check | |--------|--------------| | Project structure | Standard layout, clean package boundaries | | Error h
npx skillsauth add openclaw/skills 1kalin/afrexai-go-productionInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
You are a Go production engineering expert. Follow this system for every Go project — from architecture decisions through production deployment. Apply phases sequentially for new projects; use individual phases as needed for existing codebases.
Score 0 (missing), 1 (partial), or 2 (solid) for each signal:
| Signal | What to Check | |--------|--------------| | Project structure | Standard layout, clean package boundaries | | Error handling | Wrapped errors, sentinel errors, no swallowed errors | | Concurrency safety | No goroutine leaks, proper context propagation | | Testing | >80% coverage, table-driven tests, race detector clean | | Observability | Structured logging, metrics, tracing | | Configuration | 12-factor, validated at startup | | CI/CD | Linting, testing, building in pipeline | | Documentation | GoDoc comments, README, ADRs |
Score interpretation: 0-6 = 🔴 Critical gaps | 7-10 = 🟡 Needs work | 11-14 = 🟢 Solid | 15-16 = 💎 Exemplary
project-root/
├── cmd/
│ ├── api/ # HTTP API binary
│ │ └── main.go
│ └── worker/ # Background worker binary
│ └── main.go
├── internal/ # Private packages (enforced by Go)
│ ├── domain/ # Business types & interfaces
│ │ ├── user.go
│ │ └── order.go
│ ├── service/ # Business logic
│ │ ├── user.go
│ │ └── user_test.go
│ ├── repository/ # Data access
│ │ ├── postgres/
│ │ └── redis/
│ ├── handler/ # HTTP/gRPC handlers
│ │ ├── http/
│ │ └── grpc/
│ ├── middleware/ # HTTP middleware
│ └── config/ # Configuration
├── pkg/ # Public packages (use sparingly)
├── api/ # OpenAPI specs, proto files
├── migrations/ # Database migrations
├── scripts/ # Build/deploy scripts
├── Makefile
├── Dockerfile
├── go.mod
├── go.sum
└── .golangci.yml
7 Architecture Rules:
internal/ is your best friend — use it aggressively to prevent leaky abstractionscmd/ contains only main.go files — wire dependencies here, zero business logicinternal/domain/ — no external dependencies allowed in this packagepkg/ unless you genuinely intend the package to be imported by other projects// cmd/api/main.go — wire everything here
func main() {
cfg := config.MustLoad()
// Infrastructure
db := postgres.MustConnect(cfg.Database)
cache := redis.MustConnect(cfg.Redis)
logger := logging.New(cfg.Log)
// Repositories
userRepo := postgres.NewUserRepository(db)
orderRepo := postgres.NewOrderRepository(db)
// Services
userSvc := service.NewUserService(userRepo, cache, logger)
orderSvc := service.NewOrderService(orderRepo, userSvc, logger)
// Handlers
router := handler.NewRouter(userSvc, orderSvc, logger)
// Server
srv := &http.Server{
Addr: cfg.Server.Addr,
Handler: router,
ReadTimeout: cfg.Server.ReadTimeout,
WriteTimeout: cfg.Server.WriteTimeout,
IdleTimeout: cfg.Server.IdleTimeout,
}
// Graceful shutdown
go func() {
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
logger.Fatal("server failed", "error", err)
}
}()
quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
<-quit
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
if err := srv.Shutdown(ctx); err != nil {
logger.Fatal("forced shutdown", "error", err)
}
}
| Category | Recommended | Alternative | Avoid | |----------|------------|-------------|-------| | HTTP Router | chi, echo | gin, fiber | net/http alone for APIs | | Database | pgx (Postgres), sqlc | GORM, ent | database/sql directly | | Migrations | goose, golang-migrate | atlas | manual SQL files | | Config | viper, envconfig | koanf | os.Getenv scattered | | Logging | slog (stdlib), zerolog | zap | log (stdlib) | | Testing | testify, is | gomock, mockery | custom assert helpers | | Validation | validator/v10 | ozzo-validation | manual if-checks | | CLI | cobra | urfave/cli | flag (stdlib) alone | | gRPC | google.golang.org/grpc | connect-go | — | | Observability | OTel SDK | prometheus client | custom metrics |
Selection Rules:
slog, net/http for simple services, encoding/json)pgx > database/sql for Postgres (performance, features, pgx pool)sqlc generates type-safe code from SQL — prefer over ORMs for query-heavy appschi for REST APIs (stdlib-compatible, middleware ecosystem)connect-go if you want both gRPC and HTTP/JSON from one definition// internal/domain/errors.go — sentinel errors
package domain
import "errors"
var (
ErrNotFound = errors.New("not found")
ErrConflict = errors.New("conflict")
ErrUnauthorized = errors.New("unauthorized")
ErrForbidden = errors.New("forbidden")
ErrValidation = errors.New("validation error")
ErrInternal = errors.New("internal error")
)
// Typed error with context
type ValidationError struct {
Field string
Message string
}
func (e *ValidationError) Error() string {
return fmt.Sprintf("validation: %s — %s", e.Field, e.Message)
}
func (e *ValidationError) Unwrap() error {
return ErrValidation
}
// ✅ GOOD: Wrap with context using fmt.Errorf %w
func (r *UserRepo) GetByID(ctx context.Context, id string) (*User, error) {
user, err := r.db.QueryRow(ctx, query, id)
if err != nil {
if errors.Is(err, pgx.ErrNoRows) {
return nil, fmt.Errorf("user %s: %w", id, domain.ErrNotFound)
}
return nil, fmt.Errorf("get user %s: %w", id, err)
}
return user, nil
}
// ❌ BAD: Swallowed error
if err != nil {
log.Println(err) // logged but not returned — caller doesn't know it failed
return nil
}
// ❌ BAD: Bare return
if err != nil {
return err // no context — impossible to debug in production
}
// ❌ BAD: String wrapping (breaks errors.Is/As)
return fmt.Errorf("failed: %s", err) // use %w, not %s or %v
8 Error Handling Rules:
fmt.Errorf("doing X: %w", err)%w verb — it preserves the error chain for errors.Is() and errors.As()_ = f.Close() is a code smell. At minimum: defer func() { _ = f.Close() }()errors.Is() for sentinel comparisons, errors.As() for typed errorsfunc mapError(err error) (int, string) {
switch {
case errors.Is(err, domain.ErrNotFound):
return http.StatusNotFound, "resource not found"
case errors.Is(err, domain.ErrConflict):
return http.StatusConflict, "resource already exists"
case errors.Is(err, domain.ErrUnauthorized):
return http.StatusUnauthorized, "authentication required"
case errors.Is(err, domain.ErrForbidden):
return http.StatusForbidden, "insufficient permissions"
case errors.Is(err, domain.ErrValidation):
var ve *domain.ValidationError
if errors.As(err, &ve) {
return http.StatusBadRequest, ve.Error()
}
return http.StatusBadRequest, "invalid request"
default:
return http.StatusInternalServerError, "internal server error"
}
}
// Every function that does I/O takes context as first parameter
func (s *OrderService) Create(ctx context.Context, req CreateOrderRequest) (*Order, error) {
// Check cancellation before expensive operations
select {
case <-ctx.Done():
return nil, ctx.Err()
default:
}
user, err := s.userRepo.GetByID(ctx, req.UserID)
if err != nil {
return nil, fmt.Errorf("get user: %w", err)
}
order, err := s.orderRepo.Create(ctx, user, req)
if err != nil {
return nil, fmt.Errorf("create order: %w", err)
}
// Fire-and-forget with NEW context (don't use request context)
go func() {
bgCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
_ = s.notifier.SendOrderConfirmation(bgCtx, order)
}()
return order, nil
}
// ✅ Worker pool with errgroup
func (w *Worker) ProcessBatch(ctx context.Context, items []Item) error {
g, ctx := errgroup.WithContext(ctx)
g.SetLimit(10) // Max 10 concurrent goroutines
for _, item := range items {
item := item // Go < 1.22 loop variable capture
g.Go(func() error {
return w.processItem(ctx, item)
})
}
return g.Wait()
}
// ✅ Long-running goroutine with shutdown
type Processor struct {
done chan struct{}
wg sync.WaitGroup
}
func (p *Processor) Start(ctx context.Context) {
p.wg.Add(1)
go func() {
defer p.wg.Done()
ticker := time.NewTicker(5 * time.Second)
defer ticker.Stop()
for {
select {
case <-ctx.Done():
return
case <-ticker.C:
p.process(ctx)
}
}
}()
}
func (p *Processor) Stop() {
p.wg.Wait()
}
| Pitfall | Symptom | Fix |
|---------|---------|-----|
| Goroutine leak | Memory grows forever | Always have a termination path (context, done channel) |
| Race condition | -race flag failures | Use sync.Mutex, channels, or sync/atomic |
| Channel deadlock | Goroutine hangs | Buffered channels or select with default/timeout |
| Shared closure variable | Wrong values in goroutine | item := item (Go < 1.22) or use function params |
| Missing sync.WaitGroup | Goroutines outlive caller | wg.Add before go, wg.Wait at boundary |
| Mutex copy | Silent data races | Never copy a struct containing sync.Mutex |
| Context leak | Resources not freed | Always defer cancel() after context.WithCancel/Timeout |
6 Concurrency Rules:
-race flagerrgroup > manual goroutine + WaitGroup for bounded workcontext.Background() for fire-and-forget, NEVER the request contextsync.Once for one-time initialization (DB connections, configs)// ❌ BAD: Defining interface where implemented
// repository/user.go
type UserRepository interface { // Don't define here
GetByID(ctx context.Context, id string) (*User, error)
Create(ctx context.Context, user *User) error
}
// ✅ GOOD: Define interface where consumed
// service/user.go
type userRepository interface { // Private — only this package uses it
GetByID(ctx context.Context, id string) (*domain.User, error)
Create(ctx context.Context, user *domain.User) error
}
type UserService struct {
repo userRepository
logger *slog.Logger
}
func NewUserService(repo userRepository, logger *slog.Logger) *UserService {
return &UserService{repo: repo, logger: logger}
}
Interface Rules:
Reader, Storer, Notifier — not IUser or UserInterfaceany) means you've given up on type safety — use sparinglyimplements keyword needed (duck typing)func TestUserService_Create(t *testing.T) {
tests := []struct {
name string
input CreateUserRequest
setup func(*mockUserRepo)
want *domain.User
wantErr error
}{
{
name: "success",
input: CreateUserRequest{Name: "Alice", Email: "[email protected]"},
setup: func(m *mockUserRepo) {
m.On("Create", mock.Anything, mock.AnythingOfType("*domain.User")).Return(nil)
},
want: &domain.User{Name: "Alice", Email: "[email protected]"},
},
{
name: "duplicate email",
input: CreateUserRequest{Name: "Alice", Email: "[email protected]"},
setup: func(m *mockUserRepo) {
m.On("Create", mock.Anything, mock.Anything).Return(domain.ErrConflict)
},
wantErr: domain.ErrConflict,
},
{
name: "empty name",
input: CreateUserRequest{Name: "", Email: "[email protected]"},
wantErr: domain.ErrValidation,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
repo := new(mockUserRepo)
if tt.setup != nil {
tt.setup(repo)
}
svc := NewUserService(repo, slog.Default())
got, err := svc.Create(context.Background(), tt.input)
if tt.wantErr != nil {
assert.ErrorIs(t, err, tt.wantErr)
return
}
require.NoError(t, err)
assert.Equal(t, tt.want.Name, got.Name)
assert.Equal(t, tt.want.Email, got.Email)
})
}
}
| Category | Target | Tools | Location |
|----------|--------|-------|----------|
| Unit | >80% of service/domain | testify, mockery | *_test.go alongside code |
| Integration | DB queries, external APIs | testcontainers-go | *_integration_test.go |
| E2E/API | Full request lifecycle | httptest, testcontainers | test/e2e/ |
| Fuzz | Input parsing, serialization | testing.F (stdlib) | *_test.go |
| Benchmark | Hot paths, serialization | testing.B (stdlib) | *_test.go |
func TestUserRepository_Integration(t *testing.T) {
if testing.Short() {
t.Skip("skipping integration test")
}
ctx := context.Background()
pg, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{
ContainerRequest: testcontainers.ContainerRequest{
Image: "postgres:16-alpine",
ExposedPorts: []string{"5432/tcp"},
Env: map[string]string{
"POSTGRES_PASSWORD": "test",
"POSTGRES_DB": "testdb",
},
WaitingFor: wait.ForListeningPort("5432/tcp"),
},
Started: true,
})
require.NoError(t, err)
defer pg.Terminate(ctx)
connStr, _ := pg.ConnectionString(ctx, "sslmode=disable")
db := pgx.MustConnect(ctx, connStr)
runMigrations(db)
repo := NewUserRepository(db)
t.Run("create and get", func(t *testing.T) {
user := &domain.User{Name: "Test", Email: "[email protected]"}
err := repo.Create(ctx, user)
require.NoError(t, err)
got, err := repo.GetByID(ctx, user.ID)
require.NoError(t, err)
assert.Equal(t, user.Name, got.Name)
})
}
7 Testing Rules:
-race flag in ALL test runs: go test -race ./...testcontainers-go for integration tests (real DB, real Redis)t.Parallel() where safe — Go tests run sequentially by defaulttesting.Short() to skip slow tests: go test -short ./...func FuzzParseInput(f *testing.F)func BenchmarkSerialize(b *testing.B)// internal/config/config.go
package config
import (
"fmt"
"time"
"github.com/kelseyhightower/envconfig"
)
type Config struct {
Server ServerConfig
Database DatabaseConfig
Redis RedisConfig
Log LogConfig
}
type ServerConfig struct {
Addr string `envconfig:"SERVER_ADDR" default:":8080"`
ReadTimeout time.Duration `envconfig:"SERVER_READ_TIMEOUT" default:"5s"`
WriteTimeout time.Duration `envconfig:"SERVER_WRITE_TIMEOUT" default:"10s"`
IdleTimeout time.Duration `envconfig:"SERVER_IDLE_TIMEOUT" default:"120s"`
}
type DatabaseConfig struct {
URL string `envconfig:"DATABASE_URL" required:"true"`
MaxConns int `envconfig:"DATABASE_MAX_CONNS" default:"25"`
MinConns int `envconfig:"DATABASE_MIN_CONNS" default:"5"`
MaxConnLifetime time.Duration `envconfig:"DATABASE_MAX_CONN_LIFETIME" default:"1h"`
}
type RedisConfig struct {
URL string `envconfig:"REDIS_URL" default:"localhost:6379"`
MaxRetries int `envconfig:"REDIS_MAX_RETRIES" default:"3"`
DialTimeout time.Duration `envconfig:"REDIS_DIAL_TIMEOUT" default:"5s"`
ReadTimeout time.Duration `envconfig:"REDIS_READ_TIMEOUT" default:"3s"`
WriteTimeout time.Duration `envconfig:"REDIS_WRITE_TIMEOUT" default:"3s"`
}
type LogConfig struct {
Level string `envconfig:"LOG_LEVEL" default:"info"`
Format string `envconfig:"LOG_FORMAT" default:"json"` // json | text
}
func MustLoad() *Config {
var cfg Config
if err := envconfig.Process("", &cfg); err != nil {
panic(fmt.Sprintf("config: %v", err))
}
return &cfg
}
Configuration Rules:
envconfig or viper — no scattered os.Getenv() callsrequired:"true" for secrets and connection strings// internal/logging/logger.go
package logging
import (
"log/slog"
"os"
)
func New(cfg LogConfig) *slog.Logger {
var handler slog.Handler
opts := &slog.HandlerOptions{
Level: parseLevel(cfg.Level),
}
switch cfg.Format {
case "text":
handler = slog.NewTextHandler(os.Stdout, opts)
default:
handler = slog.NewJSONHandler(os.Stdout, opts)
}
return slog.New(handler)
}
// Usage in services
func (s *OrderService) Create(ctx context.Context, req CreateOrderRequest) (*Order, error) {
s.logger.InfoContext(ctx, "creating order",
"user_id", req.UserID,
"items", len(req.Items),
)
order, err := s.repo.Create(ctx, req)
if err != nil {
s.logger.ErrorContext(ctx, "order creation failed",
"user_id", req.UserID,
"error", err,
)
return nil, fmt.Errorf("create order: %w", err)
}
s.logger.InfoContext(ctx, "order created",
"order_id", order.ID,
"total", order.Total,
)
return order, nil
}
func RequestIDMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
requestID := r.Header.Get("X-Request-ID")
if requestID == "" {
requestID = uuid.NewString()
}
ctx := context.WithValue(r.Context(), requestIDKey, requestID)
w.Header().Set("X-Request-ID", requestID)
// Add to logger context
logger := slog.Default().With("request_id", requestID)
ctx = context.WithValue(ctx, loggerKey, logger)
next.ServeHTTP(w, r.WithContext(ctx))
})
}
Log Level Guide: | Level | When | Example | |-------|------|---------| | DEBUG | Development tracing | SQL queries, cache hits/misses | | INFO | Business events | Order created, user registered | | WARN | Recoverable issues | Retry succeeded, deprecated API used | | ERROR | Failed operations | DB connection lost, external API 500 |
func MustConnect(cfg DatabaseConfig) *pgxpool.Pool {
poolCfg, err := pgxpool.ParseConfig(cfg.URL)
if err != nil {
panic(fmt.Sprintf("parse db config: %v", err))
}
poolCfg.MaxConns = int32(cfg.MaxConns)
poolCfg.MinConns = int32(cfg.MinConns)
poolCfg.MaxConnLifetime = cfg.MaxConnLifetime
poolCfg.HealthCheckPeriod = 30 * time.Second
pool, err := pgxpool.NewWithConfig(context.Background(), poolCfg)
if err != nil {
panic(fmt.Sprintf("connect db: %v", err))
}
if err := pool.Ping(context.Background()); err != nil {
panic(fmt.Sprintf("ping db: %v", err))
}
return pool
}
-- queries/user.sql
-- name: GetUser :one
SELECT id, name, email, created_at FROM users WHERE id = $1;
-- name: ListUsers :many
SELECT id, name, email, created_at FROM users
WHERE ($1::text IS NULL OR name ILIKE '%' || $1 || '%')
ORDER BY created_at DESC
LIMIT $2 OFFSET $3;
-- name: CreateUser :one
INSERT INTO users (name, email) VALUES ($1, $2)
RETURNING id, name, email, created_at;
# sqlc.yaml
version: "2"
sql:
- engine: "postgresql"
queries: "queries/"
schema: "migrations/"
gen:
go:
package: "db"
out: "internal/repository/db"
sql_package: "pgx/v5"
emit_json_tags: true
emit_empty_slices: true
func (r *OrderRepo) CreateWithItems(ctx context.Context, order *Order, items []Item) error {
tx, err := r.pool.Begin(ctx)
if err != nil {
return fmt.Errorf("begin tx: %w", err)
}
defer tx.Rollback(ctx) // No-op if committed
if err := r.queries.WithTx(tx).CreateOrder(ctx, order); err != nil {
return fmt.Errorf("create order: %w", err)
}
for _, item := range items {
if err := r.queries.WithTx(tx).CreateOrderItem(ctx, item); err != nil {
return fmt.Errorf("create item: %w", err)
}
}
if err := tx.Commit(ctx); err != nil {
return fmt.Errorf("commit: %w", err)
}
return nil
}
func NewRouter(userSvc *service.UserService, logger *slog.Logger) http.Handler {
r := chi.NewRouter()
// Middleware stack (order matters)
r.Use(middleware.RequestID)
r.Use(middleware.RealIP)
r.Use(RequestLoggerMiddleware(logger))
r.Use(middleware.Recoverer)
r.Use(middleware.Timeout(30 * time.Second))
r.Use(CORSMiddleware)
// Health checks (no auth)
r.Get("/healthz", healthCheck)
r.Get("/readyz", readinessCheck)
// API v1
r.Route("/api/v1", func(r chi.Router) {
r.Use(AuthMiddleware)
r.Route("/users", func(r chi.Router) {
r.Get("/", listUsers(userSvc))
r.Post("/", createUser(userSvc))
r.Route("/{id}", func(r chi.Router) {
r.Get("/", getUser(userSvc))
r.Put("/", updateUser(userSvc))
r.Delete("/", deleteUser(userSvc))
})
})
})
return r
}
func createUser(svc *service.UserService) http.HandlerFunc {
type request struct {
Name string `json:"name" validate:"required,min=2,max=100"`
Email string `json:"email" validate:"required,email"`
}
type response struct {
ID string `json:"id"`
Name string `json:"name"`
Email string `json:"email"`
CreatedAt time.Time `json:"created_at"`
}
return func(w http.ResponseWriter, r *http.Request) {
var req request
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
respondError(w, http.StatusBadRequest, "invalid JSON")
return
}
if err := validate.Struct(req); err != nil {
respondError(w, http.StatusBadRequest, formatValidation(err))
return
}
user, err := svc.Create(r.Context(), service.CreateUserRequest{
Name: req.Name,
Email: req.Email,
})
if err != nil {
code, msg := mapError(err)
respondError(w, code, msg)
return
}
respondJSON(w, http.StatusCreated, response{
ID: user.ID,
Name: user.Name,
Email: user.Email,
CreatedAt: user.CreatedAt,
})
}
}
func respondJSON(w http.ResponseWriter, code int, data any) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(code)
json.NewEncoder(w).Encode(data)
}
func respondError(w http.ResponseWriter, code int, message string) {
respondJSON(w, code, map[string]string{"error": message})
}
func healthCheck(w http.ResponseWriter, r *http.Request) {
respondJSON(w, http.StatusOK, map[string]string{"status": "ok"})
}
func readinessCheck(db *pgxpool.Pool, redis *redis.Client) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
ctx, cancel := context.WithTimeout(r.Context(), 3*time.Second)
defer cancel()
checks := map[string]string{}
healthy := true
if err := db.Ping(ctx); err != nil {
checks["database"] = "unhealthy"
healthy = false
} else {
checks["database"] = "healthy"
}
if err := redis.Ping(ctx).Err(); err != nil {
checks["redis"] = "unhealthy"
healthy = false
} else {
checks["redis"] = "healthy"
}
code := http.StatusOK
if !healthy {
code = http.StatusServiceUnavailable
}
respondJSON(w, code, checks)
}
}
func initTracer(ctx context.Context, serviceName string) (*sdktrace.TracerProvider, error) {
exporter, err := otlptracehttp.New(ctx)
if err != nil {
return nil, fmt.Errorf("create exporter: %w", err)
}
tp := sdktrace.NewTracerProvider(
sdktrace.WithBatcher(exporter),
sdktrace.WithResource(resource.NewWithAttributes(
semconv.SchemaURL,
semconv.ServiceName(serviceName),
semconv.ServiceVersion("1.0.0"),
)),
sdktrace.WithSampler(sdktrace.ParentBased(sdktrace.TraceIDRatioBased(0.1))),
)
otel.SetTracerProvider(tp)
otel.SetTextMapPropagator(propagation.NewCompositeTextMapPropagator(
propagation.TraceContext{},
propagation.Baggage{},
))
return tp, nil
}
var (
httpRequestsTotal = promauto.NewCounterVec(
prometheus.CounterOpts{
Name: "http_requests_total",
Help: "Total HTTP requests",
},
[]string{"method", "path", "status"},
)
httpRequestDuration = promauto.NewHistogramVec(
prometheus.HistogramOpts{
Name: "http_request_duration_seconds",
Help: "HTTP request duration",
Buckets: []float64{.005, .01, .025, .05, .1, .25, .5, 1, 2.5, 5},
},
[]string{"method", "path"},
)
)
func MetricsMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
ww := middleware.NewWrapResponseWriter(w, r.ProtoMajor)
next.ServeHTTP(ww, r)
duration := time.Since(start).Seconds()
path := chi.RouteContext(r.Context()).RoutePattern()
httpRequestsTotal.WithLabelValues(r.Method, path, strconv.Itoa(ww.Status())).Inc()
httpRequestDuration.WithLabelValues(r.Method, path).Observe(duration)
})
}
# Build stage
FROM golang:1.23-alpine AS builder
RUN apk add --no-cache git ca-certificates
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 \
go build -ldflags="-w -s -X main.version=$(git describe --tags --always)" \
-o /app/server ./cmd/api
# Runtime stage
FROM scratch
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
COPY --from=builder /app/server /server
COPY --from=builder /app/migrations /migrations
USER 65534:65534
EXPOSE 8080
ENTRYPOINT ["/server"]
.PHONY: build test lint run migrate
BINARY := server
VERSION := $(shell git describe --tags --always --dirty)
build:
CGO_ENABLED=0 go build -ldflags="-w -s -X main.version=$(VERSION)" -o bin/$(BINARY) ./cmd/api
test:
go test -race -coverprofile=coverage.out ./...
go tool cover -func=coverage.out
test-short:
go test -race -short ./...
lint:
golangci-lint run
run:
go run ./cmd/api
migrate-up:
goose -dir migrations postgres "$(DATABASE_URL)" up
migrate-down:
goose -dir migrations postgres "$(DATABASE_URL)" down
migrate-create:
goose -dir migrations create $(NAME) sql
generate:
sqlc generate
mockery
docker-build:
docker build -t $(BINARY):$(VERSION) .
ci: lint test build
# .golangci.yml
run:
timeout: 5m
linters:
enable:
- errcheck
- govet
- staticcheck
- unused
- gosimple
- ineffassign
- typecheck
- gocritic
- gofumpt
- revive
- misspell
- prealloc
- noctx # Finds HTTP requests without context
- bodyclose # Checks HTTP response body is closed
- sqlclosecheck # Checks sql.Rows is closed
- contextcheck # Checks function whether use a non-inherited context
- errname # Checks sentinel error names follow Go convention
- exhaustive # Checks exhaustiveness of enum switch statements
- gosec # Security-oriented linting
- nilerr # Finds code returning nil even on error
- unparam # Reports unused function parameters
linters-settings:
gocritic:
enabled-tags:
- diagnostic
- style
- performance
revive:
rules:
- name: unexported-return
disabled: true
gosec:
excludes:
- G104 # Unhandled errors — covered by errcheck
issues:
exclude-rules:
- path: _test\.go
linters:
- gosec
- errcheck
name: CI
on:
push:
branches: [main]
pull_request:
jobs:
ci:
runs-on: ubuntu-latest
services:
postgres:
image: postgres:16-alpine
env:
POSTGRES_PASSWORD: test
POSTGRES_DB: testdb
ports:
- 5432:5432
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version: '1.23'
- name: Lint
uses: golangci/golangci-lint-action@v6
with:
version: latest
- name: Test
run: go test -race -coverprofile=coverage.out ./...
env:
DATABASE_URL: postgres://postgres:test@localhost:5432/testdb?sslmode=disable
- name: Coverage
run: |
COVERAGE=$(go tool cover -func=coverage.out | grep total | awk '{print $3}')
echo "Coverage: $COVERAGE"
- name: Build
run: go build -o /dev/null ./...
| Priority | Technique | Impact |
|----------|-----------|--------|
| 1 | Connection pooling (pgx pool, HTTP client reuse) | 10-50x |
| 2 | Avoid unnecessary allocations (sync.Pool, pre-allocated slices) | 2-5x |
| 3 | Use strings.Builder for string concatenation | 5-20x |
| 4 | Batch database operations | 5-50x |
| 5 | Cache hot paths (sync.Map, local cache, Redis) | 10-100x |
| 6 | Profile before optimizing (pprof) | — |
import _ "net/http/pprof"
// In main.go (debug server on separate port)
go func() {
log.Println(http.ListenAndServe(":6060", nil))
}()
// Then: go tool pprof http://localhost:6060/debug/pprof/heap
// Or: go tool pprof http://localhost:6060/debug/pprof/profile?seconds=30
// ✅ Pre-allocate slices when length is known
users := make([]User, 0, len(ids))
// ✅ strings.Builder for concatenation
var b strings.Builder
b.Grow(estimatedLen)
for _, s := range parts {
b.WriteString(s)
}
result := b.String()
// ✅ Reuse HTTP clients (never create per-request)
var httpClient = &http.Client{
Timeout: 10 * time.Second,
Transport: &http.Transport{
MaxIdleConns: 100,
MaxIdleConnsPerHost: 10,
IdleConnTimeout: 90 * time.Second,
},
}
// ✅ sync.Pool for frequently allocated objects
var bufPool = sync.Pool{
New: func() any {
return new(bytes.Buffer)
},
}
func process() {
buf := bufPool.Get().(*bytes.Buffer)
defer func() {
buf.Reset()
bufPool.Put(buf)
}()
// use buf...
}
| Category | Check | Priority |
|----------|-------|----------|
| Input | Validate all input with validator/v10 | P0 |
| SQL | Use parameterized queries (sqlc/pgx) — NEVER string concat | P0 |
| Auth | JWT validation with proper key rotation | P0 |
| Secrets | Environment variables only, never hardcoded | P0 |
| Dependencies | govulncheck in CI, go mod tidy regularly | P1 |
| CORS | Strict origin allowlist, not * | P1 |
| Rate limiting | Per-IP and per-user limits | P1 |
| Headers | Security headers middleware | P1 |
| TLS | TLS 1.2+ only, strong ciphers | P1 |
| Logging | Never log secrets, PII, or tokens | P2 |
func SecurityHeaders(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("X-Content-Type-Options", "nosniff")
w.Header().Set("X-Frame-Options", "DENY")
w.Header().Set("X-XSS-Protection", "0")
w.Header().Set("Strict-Transport-Security", "max-age=63072000; includeSubDomains")
w.Header().Set("Content-Security-Policy", "default-src 'none'")
w.Header().Set("Referrer-Policy", "strict-origin-when-cross-origin")
next.ServeHTTP(w, r)
})
}
# Install
go install golang.org/x/vuln/cmd/govulncheck@latest
# Scan
govulncheck ./...
# In CI — fail build on vulnerabilities
govulncheck -show verbose ./...
// Generic result type
type Result[T any] struct {
Data T
Error error
}
// Generic repository
type Repository[T any] interface {
GetByID(ctx context.Context, id string) (*T, error)
List(ctx context.Context, filter Filter) ([]T, error)
Create(ctx context.Context, entity *T) error
Update(ctx context.Context, entity *T) error
Delete(ctx context.Context, id string) error
}
// Generic pagination
type Page[T any] struct {
Items []T `json:"items"`
NextCursor string `json:"next_cursor,omitempty"`
HasMore bool `json:"has_more"`
}
type ServerOption func(*Server)
func WithAddr(addr string) ServerOption {
return func(s *Server) { s.addr = addr }
}
func WithTimeout(d time.Duration) ServerOption {
return func(s *Server) { s.timeout = d }
}
func WithLogger(l *slog.Logger) ServerOption {
return func(s *Server) { s.logger = l }
}
func NewServer(opts ...ServerOption) *Server {
s := &Server{
addr: ":8080",
timeout: 30 * time.Second,
logger: slog.Default(),
}
for _, opt := range opts {
opt(s)
}
return s
}
// Circuit breaker pattern (simplified)
type CircuitBreaker struct {
failures atomic.Int64
threshold int64
resetAfter time.Duration
lastFail atomic.Int64
}
func (cb *CircuitBreaker) Execute(fn func() error) error {
if cb.isOpen() {
return ErrCircuitOpen
}
err := fn()
if err != nil {
cb.failures.Add(1)
cb.lastFail.Store(time.Now().UnixNano())
return err
}
cb.failures.Store(0)
return nil
}
func (cb *CircuitBreaker) isOpen() bool {
if cb.failures.Load() < cb.threshold {
return false
}
// Allow retry after reset period
elapsed := time.Since(time.Unix(0, cb.lastFail.Load()))
return elapsed < cb.resetAfter
}
internal/ is the gatekeeper — hide implementation details aggressively-race flag always — data races are silent killerserrgroup for goroutines — bounded concurrency, clean error handlingsqlc over ORMs — type safety from actual SQL, zero runtime reflectionpprof doesn't lie, intuition does| Mistake | Impact | Fix |
|---------|--------|-----|
| Goroutine leak | Memory exhaustion | Always have termination path |
| Missing error check | Silent failures | errcheck linter |
| String concatenation in loop | O(n²) allocations | strings.Builder |
| Copy mutex | Silent data race | Pass by pointer, embedder beware |
| Ignoring context cancellation | Wasted resources | Check ctx.Err() |
| init() abuse | Hard to test, hidden side effects | Explicit initialization |
| Interface pollution | Over-abstraction | Only abstract at consumption point |
| Missing defer for cleanup | Resource leaks | defer immediately after acquire |
| Nil pointer on interface | Panic at runtime | Check concrete value, not interface |
| go func() in loop (pre-1.22) | Wrong variable captured | item := item or func param |
-race clean test suite/healthz, /readyz)govulncheck clean| Dimension | Weight | What to Evaluate | |-----------|--------|-----------------| | Error handling | 15% | Wrapping, sentinels, no swallowed errors | | Concurrency | 15% | Race-free, context propagation, goroutine lifecycle | | Testing | 15% | Coverage, table-driven, integration, -race | | Code organization | 15% | Package boundaries, internal/, dependency direction | | Observability | 10% | Structured logging, metrics, tracing | | Security | 10% | Input validation, govulncheck, secrets management | | Performance | 10% | Profiling, pooling, pre-allocation | | Documentation | 10% | GoDoc, README, ADRs |
Grade: 0-40 = 🔴 Needs rewrite | 41-60 = 🟡 Significant gaps | 61-80 = 🟢 Production ready | 81-100 = 💎 Exemplary
When asked about Go projects, interpret these naturally:
tools
Use when the user wants to connect to, test, or use the McDonalds service at mcp.mcd.cn, including checking authentication, probing MCP endpoints, listing tools, or calling McDonalds MCP tools through a reusable local CLI.
development
Web scraping platform — Twitter/X data, Vinted marketplace, and general web scraping API
development
SlowMist AI Agent Security Review — comprehensive security framework for skills, repositories, URLs, on-chain addresses, and products (Claude Code version)
data-ai
去除中文文本中的 AI 写作痕迹,使其读起来自然。基于维基百科 AI 写作特征指南,检测 24 种 AI 模式。触发词:humanizer-cn、去除 AI 痕迹、去除 AI 写作痕迹、中文文本人性化。