技能详情(站内镜像,无评论)
许可证:MIT-0
MIT-0 ·免费使用、修改和重新分发。无需归因。
版本:v1.0.0
统计:⭐ 0 · 354 · 1 current installs · 1 all-time installs
⭐ 0
安装量(当前) 1
🛡 VirusTotal :良性 · OpenClaw :良性
Package:1kalin/afrexai-go-production
安全扫描(ClawHub)
- VirusTotal :良性
- OpenClaw :良性
OpenClaw 评估
The skill is an instruction-only Go production-engineering guide whose requirements, instructions, and assets are coherent with its stated purpose and do not request additional privileges, installs, or credentials.
目的
Name/description (Go production engineering) matches the content: SKILL.md and README provide phased guidance, code examples, checklists, and scaffolding instructions. There are no unrelated requested binaries, env vars, or config paths that would be inconsistent with the stated purpose.
说明范围
SKILL.md is a long, prescriptive text with architecture patterns, code snippets, and checklists. It does not instruct the agent to read arbitrary system files, access secrets, or call external endpoints beyond recommending libraries and CI/CD patterns. No vague 'gather whatever context you need' directives were found; the instructions stay within code review/scaffold/advice scope.
安装机制
There is no install spec and no code files that would be written or executed on install. This is the lower-risk instruction-only pattern — nothing is downloaded or extracted.
证书
The skill declares no required environment variables, credentials, or config paths. The guidance references common third-party libraries and CI practices but does not request unrelated secrets or multiple external credentials.
持久
always is false and the skill does not request persistent system presence or modifications to other skills or global agent settings. Autonomous invocation is allowed by platform default but is not combined with other red flags.
综合结论
This skill is a content-only production-engineering guide for Go and appears coherent with its description. Because it is instruction-only, installing it does not place new binaries or code on disk. Still consider: review any scaffolding or code the agent generates before committing it; do not supply credentials (GitHub, cloud keys) to the agent unless you explicitly intend to grant repository or deployment access; vet third-party library reco…
安装(复制给龙虾 AI)
将下方整段复制到龙虾中文库对话中,由龙虾按 SKILL.md 完成安装。
请把本段交给龙虾中文库(龙虾 AI)执行:为本机安装 OpenClaw 技能「Go Production Engineering」。简介:Expertise in Go project architecture, error handling, concurrency safety, testi…。
请 fetch 以下地址读取 SKILL.md 并按文档完成安装:https://raw.githubusercontent.com/openclaw/skills/refs/heads/main/skills/1kalin/afrexai-go-production/SKILL.md
(来源:yingzhi8.cn 技能库)
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 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
---
## Phase 1: Project Architecture
### Project Structure (Standard Layout)
```
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:**
1. `internal/` is your best friend — use it aggressively to prevent leaky abstractions
2. `cmd/` contains only `main.go` files — wire dependencies here, zero business logic
3. Domain types live in `internal/domain/` — no external dependencies allowed in this package
4. Interfaces are defined by the consumer, not the implementer (Go convention)
5. One package = one responsibility. If you can't name it in one word, split it
6. Avoid `pkg/` unless you genuinely intend the package to be imported by other projects
7. Circular imports are compile errors in Go — design your dependency graph as a DAG
### Dependency Injection Pattern
```go
// 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)
}
}
```
### Framework & Library Selection
| 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:**
1. Prefer stdlib when it's good enough (`slog`, `net/http` for simple services, `encoding/json`)
2. `pgx` > `database/sql` for Postgres (performance, features, pgx pool)
3. `sqlc` generates type-safe code from SQL — prefer over ORMs for query-heavy apps
4. Use `chi` for REST APIs (stdlib-compatible, middleware ecosystem)
5. For gRPC, use `connect-go` if you want both gRPC and HTTP/JSON from one definition
---
## Phase 2: Error Handling
### Error Architecture
```go
// 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
}
```
### Error Wrapping Rules
```go
// ✅ 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:**
1. Always wrap errors with context: `fmt.Errorf("doing X: %w", err)`
2. Use `%w` verb — it preserves the error chain for `errors.Is()` and `errors.As()`
3. Define sentinel errors in the domain package for business-level errors
4. Handle errors at the boundary (HTTP handler) — map to status codes there
5. Never ignore errors: `_ = f.Close()` is a code smell. At minimum: `defer func() { _ = f.Close() }()`
6. Use `errors.Is()` for sentinel comparisons, `errors.As()` for typed errors
7. Don't log AND return an error — pick one (usually return; log at the top)
8. Panics are for programmer errors only (impossible states) — never for runtime errors
### HTTP Error Response Mapping
```go
func 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"
}
}
```
---
## Phase 3: Concurrency Patterns
### Context Propagation (Non-Negotiable)
```go
// 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
}
```
### Goroutine Lifecycle Management
```go
// ✅ 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()
}
```
### Common Concurrency Pitfalls
| 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:**
1. Always run tests with `-race` flag
2. `errgroup` > manual goroutine + WaitGroup for bounded work
3. Channels for communication, mutexes for state protection — pick one per use case
4. Never start a goroutine without a plan for how it stops
5. Use `context.Background()` for fire-and-forget, NEVER the request context
6. `sync.Once` for one-time initialization (DB connections, configs)
---
## Phase 4: Interface Design
### Consumer-Defined Interfaces (Go Convention)
```go
// ❌ 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:**
1. Accept interfaces, return structs
2. Keep interfaces small — 1-3 methods ideal
3. Name interfaces by what they do: `Reader`, `Storer`, `Notifier` — not `IUser` or `UserInterface`
4. The empty interface (`any`) means you've given up on type safety — use sparingly
5. Interfaces are satisfied implicitly — no `implements` keyword needed (duck typing)
---
## Phase 5: Testing
### Table-Driven Tests (The Go Way)
```go
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: "alice@example.com"},
setup: func(m *mockUserRepo) {
m.On("Create", mock.Anything, mock.AnythingOfType("*domain.User")).Return(nil)
},
want: &domain.User{Name: "Alice", Email: "alice@example.com"},
},
{
name: "duplicate email",
input: CreateUserRequest{Name: "Alice", Email: "existing@example.com"},
setup: func(m *mockUserRepo) {
m.On("Create", mock.Anything, mock.Anything).Return(domain.ErrConflict)
},
wantErr: domain.ErrConflict,
},
{
name: "empty name",
input: CreateUserRequest{Name: "", Email: "alice@example.com"},
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)
})
}
}
```
### Test Categories & Targets
| 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` |
### Integration Testing with testcontainers
```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: "test@example.com"}
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:**
1. `-race` flag in ALL test runs: `go test -race ./...`
2. Table-driven tests for anything with >2 cases
3. `testcontainers-go` for integration tests (real DB, real Redis)
4. Use `t.Parallel()` where safe — Go tests run sequentially by default
5. `testing.Short()` to skip slow tests: `go test -short ./...`
6. Fuzz critical parsing code: `func FuzzParseInput(f *testing.F)`
7. Benchmark hot paths: `func BenchmarkSerialize(b *testing.B)`
---
## Phase 6: Configuration & Startup
### 12-Factor Configuration
```go
// 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:**
1. Validate ALL config at startup — fail fast, not at 3 AM
2. Use `envconfig` or `viper` — no scattered `os.Getenv()` calls
3. Provide sensible defaults for non-secret values
4. `required:"true"` for secrets and connection strings
5. Never log secrets — redact in String() methods
---
## Phase 7: Structured Logging
### slog (Go 1.21+ stdlib)
```go
// 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
}
```
### Request ID Middleware
```go
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 |
---
## Phase 8: Database Patterns
### pgx Connection Pool
```go
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
}
```
### sqlc Pattern (Type-Safe SQL)
```sql
-- 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;
```
```yaml
# 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
```
### Transaction Pattern
```go
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
}
```
---
## Phase 9: HTTP API Design
### Router Setup with chi
```go
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
}
```
### Request/Response Pattern
```go
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})
}
```
### Health Check Pattern
```go
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)
}
}
```
---
## Phase 10: Observability (OpenTelemetry)
### OTel Setup
```go
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
}
```
### Metrics with Prometheus
```go
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)
})
}
```
---
## Phase 11: Production Deployment
### Multi-Stage Dockerfile
```dockerfile
# 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"]
```
### Makefile
```makefile
.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-lint Configuration
```yaml
# .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
```
### GitHub Actions CI
```yaml
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 ./...
```
---
## Phase 12: Performance Optimization
### Priority Stack
| 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`) | — |
### Profiling
```go
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
```
### Common Optimizations
```go
// ✅ 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...
}
```
---
## Phase 13: Security Hardening
### Security Checklist
| 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 |
### Security Headers Middleware
```go
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)
})
}
```
### Vulnerability Scanning
```bash
# Install
go install golang.org/x/vuln/cmd/govulncheck@latest
# Scan
govulncheck ./...
# In CI — fail build on vulnerabilities
govulncheck -show verbose ./...
```
---
## Phase 14: Advanced Patterns
### Generics (Go 1.18+)
```go
// 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"`
}
```
### Functional Options Pattern
```go
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
}
```
### Graceful Degradation
```go
// 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
}
```
---
## 10 Go Production Commandments
1. **`internal/` is the gatekeeper** — hide implementation details aggressively
2. **Errors are values** — wrap them, check them, never ignore them
3. **`-race` flag always** — data races are silent killers
4. **Interfaces at the consumer** — small, focused, implicit
5. **Context everywhere** — first param for anything doing I/O
6. **`errgroup` for goroutines** — bounded concurrency, clean error handling
7. **`sqlc` over ORMs** — type safety from actual SQL, zero runtime reflection
8. **Profile before optimizing** — `pprof` doesn't lie, intuition does
9. **Fail at startup** — validate config, check connections, panic early
10. **Graceful shutdown** — catch signals, drain connections, close cleanly
---
## 10 Common Go Mistakes
| 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 |
---
## Production Readiness Checklist
### Mandatory (P0)
- [ ] `-race` clean test suite
- [ ] >80% test coverage on business logic
- [ ] Structured logging (slog/zerolog)
- [ ] Graceful shutdown with signal handling
- [ ] Health check endpoints (`/healthz`, `/readyz`)
- [ ] Configuration validation at startup
- [ ] Error wrapping with context throughout
- [ ] golangci-lint clean (strict config)
- [ ] Multi-stage Docker build (scratch/distroless)
- [ ] `govulncheck` clean
### Recommended (P1)
- [ ] OpenTelemetry tracing
- [ ] Prometheus metrics
- [ ] Request ID propagation
- [ ] Rate limiting
- [ ] Security headers
- [ ] Integration tests with testcontainers
- [ ] Database migrations (goose/migrate)
- [ ] CI/CD pipeline (lint → test → build → deploy)
---
## Quality Scoring (0-100)
| 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
---
## Natural Language Commands
When asked about Go projects, interpret these naturally:
- "Review this Go code" → Run quick health check, identify anti-patterns
- "Set up a new Go service" → Generate full project structure with all phases
- "Fix the error handling" → Apply Phase 2 patterns throughout
- "Add tests" → Generate table-driven tests following Phase 5
- "Make this production ready" → Run through production readiness checklist
- "Profile this" → Guide through pprof analysis
- "Add observability" → Apply Phase 10 (OTel + Prometheus)
- "Optimize performance" → Profile first, then apply Phase 12 priority stack
- "Set up CI" → Generate GitHub Actions + golangci-lint config
- "Add database" → pgx pool + sqlc + migration setup
- "Review architecture" → Evaluate against Phase 1 rules
- "Security audit" → Run through Phase 13 checklist