# Start Redis (optional — service works without it)
docker run -d --name redis-cap-lab -p 16379:6379 redis:7-alpine
# Build and run
docker run --rm --network=host golang:1.22-alpine sh -c "
mkdir -p /tmp/capstone
cd /tmp/capstone
cat > go.mod << 'EOF'
module capstone
go 1.22
EOF
go get github.com/sony/[email protected]go get modernc.org/[email protected]go get github.com/redis/go-redis/v9
# ... (write all files as shown in steps 3-7)
go mod tidy
go test ./... -v
go run .
"
docker run --rm --network=host golang:1.22-alpine sh -c "
mkdir -p /tmp/cap
cd /tmp/cap
cat > go.mod << 'EOF'
module capstone
go 1.22
EOF
go get github.com/sony/[email protected] modernc.org/[email protected] github.com/redis/go-redis/v9 2>/dev/null
cat > main_test.go << 'GOEOF'
package main
import \"testing\"
func TestProductValidation(t *testing.T) {
tests := []struct{ name string; product Product; wantErr bool }{
{\"valid\", Product{\"1\", \"Book\", 9.99}, false},
{\"empty name\", Product{\"2\", \"\", 9.99}, true},
{\"zero price\", Product{\"3\", \"Book\", 0}, true},
{\"negative price\", Product{\"4\", \"Book\", -1}, true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := validateProduct(&tt.product)
if (err != nil) != tt.wantErr { t.Errorf(\"got %v wantErr=%v\", err, tt.wantErr) }
})
}
}
GOEOF
cat > main.go << 'GOEOF'
package main
import (
\"context\"
\"database/sql\"
\"encoding/json\"
\"errors\"
\"fmt\"
\"log/slog\"
\"net/http\"
\"os\"
\"os/signal\"
\"sync\"
\"syscall\"
\"time\"
_ \"modernc.org/sqlite\"
\"github.com/sony/gobreaker\"
\"github.com/redis/go-redis/v9\"
)
type Product struct { ID, Name string; Price float64 }
func validateProduct(p *Product) error {
if p.Name == \"\" { return errors.New(\"name required\") }
if p.Price <= 0 { return errors.New(\"price must be positive\") }
return nil
}
var ErrNotFound = errors.New(\"not found\")
type ProductDB struct{ db *sql.DB }
func NewProductDB() (*ProductDB, error) {
db, err := sql.Open(\"sqlite\", \":memory:\")
if err != nil { return nil, err }
_, err = db.Exec(\`CREATE TABLE IF NOT EXISTS products (id TEXT PRIMARY KEY, name TEXT NOT NULL, price REAL)\`)
return &ProductDB{db}, err
}
func (p *ProductDB) Save(ctx context.Context, prod *Product) error {
_, err := p.db.ExecContext(ctx, \"INSERT OR REPLACE INTO products VALUES (?,?,?)\", prod.ID, prod.Name, prod.Price)
return err
}
func (p *ProductDB) List(ctx context.Context) ([]*Product, error) {
rows, err := p.db.QueryContext(ctx, \"SELECT id,name,price FROM products ORDER BY name\")
if err != nil { return nil, err }; defer rows.Close()
var ps []*Product
for rows.Next() { var pr Product; rows.Scan(&pr.ID,&pr.Name,&pr.Price); ps = append(ps, &pr) }
return ps, nil
}
func main() {
logger := slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelInfo}))
ctx := context.Background()
db, err := NewProductDB()
if err != nil { logger.Error(\"db init failed\", \"err\", err); os.Exit(1) }
db.Save(ctx, &Product{\"1\",\"Go Book\",39.99})
db.Save(ctx, &Product{\"2\",\"K8s Guide\",59.99})
logger.Info(\"sqlite initialized (pure Go, no CGO)\")
rdb := redis.NewClient(&redis.Options{Addr: \"localhost:16379\"})
rdbOK := rdb.Ping(ctx).Err() == nil
if rdbOK { logger.Info(\"redis connected\", \"addr\", \"localhost:16379\") } else { logger.Info(\"redis unavailable, cache disabled\") }
cb := gobreaker.NewCircuitBreaker(gobreaker.Settings{
Name: \"product-db\",
ReadyToTrip: func(c gobreaker.Counts) bool { return c.ConsecutiveFailures >= 3 },
OnStateChange: func(n string, f, t gobreaker.State) {
logger.Warn(\"cb state change\", \"from\", f.String(), \"to\", t.String())
},
})
listWithCB := func(r *http.Request) ([]*Product, error) {
if rdbOK {
if val, err := rdb.Get(r.Context(), \"products:all\").Result(); err == nil {
var ps []*Product; json.Unmarshal([]byte(val), &ps)
logger.Info(\"cache hit\"); return ps, nil
}
}
res, err := cb.Execute(func() (interface{}, error) { return db.List(r.Context()) })
if err != nil { return nil, err }
ps := res.([]*Product)
if rdbOK { data, _ := json.Marshal(ps); rdb.Set(r.Context(), \"products:all\", string(data), 30*time.Second) }
logger.Info(\"db query\", \"count\", len(ps), \"cb\", cb.State().String())
return ps, nil
}
mux := http.NewServeMux()
mux.HandleFunc(\"/health\", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set(\"Content-Type\", \"application/json\")
json.NewEncoder(w).Encode(map[string]string{\"status\":\"healthy\",\"time\":time.Now().Format(time.RFC3339)})
})
mux.HandleFunc(\"/ready\", func(w http.ResponseWriter, r *http.Request) {
json.NewEncoder(w).Encode(map[string]bool{\"ready\":true})
})
mux.HandleFunc(\"/products\", func(w http.ResponseWriter, r *http.Request) {
ps, err := listWithCB(r)
if err != nil { http.Error(w, err.Error(), 500); return }
if ps == nil { ps = []*Product{} }
w.Header().Set(\"Content-Type\", \"application/json\"); json.NewEncoder(w).Encode(ps)
})
srv := &http.Server{Addr: \":18089\", Handler: mux, ReadTimeout: 5*time.Second, WriteTimeout: 10*time.Second}
var wg sync.WaitGroup; wg.Add(1)
go func() {
defer wg.Done()
logger.Info(\"server starting\", \"addr\", srv.Addr)
if err := srv.ListenAndServe(); !errors.Is(err, http.ErrServerClosed) {
logger.Error(\"server error\", \"err\", err)
}
}()
time.Sleep(100 * time.Millisecond)
// Integration tests
fmt.Println(\"=== Integration Tests ===\")
r1, _ := http.Get(\"http://localhost:18089/health\")
var h map[string]string; json.NewDecoder(r1.Body).Decode(&h); r1.Body.Close()
fmt.Printf(\"GET /health -> status=%s\\n\", h[\"status\"])
r2, _ := http.Get(\"http://localhost:18089/products\")
var ps []*Product; json.NewDecoder(r2.Body).Decode(&ps); r2.Body.Close()
fmt.Printf(\"GET /products -> %d items\\n\", len(ps))
for _, p := range ps { fmt.Printf(\" %-12s \$%.2f\\n\", p.Name, p.Price) }
if rdbOK {
r3, _ := http.Get(\"http://localhost:18089/products\")
r3.Body.Close()
fmt.Println(\"GET /products (2nd) -> should be cache hit (see logs above)\")
rdb.Del(ctx, \"products:all\")
}
quit := make(chan os.Signal,1); signal.Notify(quit, syscall.SIGTERM, syscall.SIGINT)
sc, cancel := context.WithTimeout(context.Background(), 5*time.Second); defer cancel()
srv.Shutdown(sc); wg.Wait()
logger.Info(\"graceful shutdown complete\")
fmt.Println(\"=== Done ===\")
}
GOEOF
go mod tidy 2>/dev/null
echo '--- Unit Tests ---'
go test ./... -v 2>&1 | grep -E 'RUN|PASS|FAIL|ok'
echo '--- Running Service ---'
go run main.go 2>&1 | grep -v 'pool.go'
"
--- Unit Tests ---
=== RUN TestProductValidation
=== RUN TestProductValidation/valid
=== RUN TestProductValidation/empty_name
=== RUN TestProductValidation/zero_price
=== RUN TestProductValidation/negative_price
--- PASS: TestProductValidation (0.00s)
--- PASS: TestProductValidation/valid (0.00s)
--- PASS: TestProductValidation/empty_name (0.00s)
--- PASS: TestProductValidation/zero_price (0.00s)
--- PASS: TestProductValidation/negative_price (0.00s)
PASS
ok capstone 0.017s
--- Running Service ---
{"time":"...","level":"INFO","msg":"sqlite initialized (pure Go, no CGO)"}
{"time":"...","level":"INFO","msg":"redis connected","addr":"localhost:16379"}
{"time":"...","level":"INFO","msg":"server starting","addr":":18089"}
=== Integration Tests ===
GET /health -> status=healthy
{"time":"...","level":"INFO","msg":"db query","count":2,"cb":"closed"}
GET /products -> 2 items
Go Book $39.99
K8s Guide $59.99
{"time":"...","level":"INFO","msg":"cache hit"}
GET /products (2nd) -> should be cache hit (see logs above)
{"time":"...","level":"INFO","msg":"graceful shutdown complete"}
=== Done ===
{"time":"2026-03-06T18:56:19Z","level":"INFO","msg":"sqlite initialized (pure Go, no CGO)"}
{"time":"2026-03-06T18:56:21Z","level":"INFO","msg":"redis unavailable, cache disabled"}
{"time":"2026-03-06T18:56:21Z","level":"INFO","msg":"server starting","addr":":18087"}
Health: map[status:healthy]
{"time":"2026-03-06T18:56:22Z","level":"INFO","msg":"db query","count":2,"cb":"closed"}
Products: 2 items
Go Book $39.99
K8s Guide $59.99
{"time":"2026-03-06T18:56:22Z","level":"INFO","msg":"shutdown complete"}
Done