sazardev/goca
GitHub生成极简且精确的 Conventional Commits 格式提交信息。遵循“为什么重于是什么”原则,严格限制主题长度并去除冗余噪音。仅在必要时添加正文解释原因或变更影响,不包含 AI 标识或文件重述,直接输出可粘贴的代码块。
安装全部 Skills
npx skills add sazardev/goca --all -g -y
更多选项
预览集合内 Skills
npx skills add sazardev/goca --list
集合内 Skills (5)
.agents/skills/caveman-commit/SKILL.md
npx skills add sazardev/goca --skill caveman-commit -g -y
SKILL.md
Frontmatter
{
"name": "caveman-commit",
"description": "Ultra-compressed commit message generator. Cuts noise from commit messages while preserving intent and reasoning. Conventional Commits format. Subject ≤50 chars, body only when \"why\" isn't obvious. Use when user says \"write a commit\", \"commit message\", \"generate commit\", \"\/commit\", or invokes \/caveman-commit. Auto-triggers when staging changes."
}
Write commit messages terse and exact. Conventional Commits format. No fluff. Why over what.
Rules
Subject line:
<type>(<scope>): <imperative summary>—<scope>optional- Types:
feat,fix,refactor,perf,docs,test,chore,build,ci,style,revert - Imperative mood: "add", "fix", "remove" — not "added", "adds", "adding"
- ≤50 chars when possible, hard cap 72
- No trailing period
- Match project convention for capitalization after the colon
Body (only if needed):
- Skip entirely when subject is self-explanatory
- Add body only for: non-obvious why, breaking changes, migration notes, linked issues
- Wrap at 72 chars
- Bullets
-not* - Reference issues/PRs at end:
Closes #42,Refs #17
What NEVER goes in:
- "This commit does X", "I", "we", "now", "currently" — the diff says what
- "As requested by..." — use Co-authored-by trailer
- "Generated with Claude Code" or any AI attribution
- Emoji (unless project convention requires)
- Restating the file name when scope already says it
Examples
Diff: new endpoint for user profile with body explaining the why
- ❌ "feat: add a new endpoint to get user profile information from the database"
- ✅
feat(api): add GET /users/:id/profile Mobile client needs profile data without the full user payload to reduce LTE bandwidth on cold-launch screens. Closes #128
Diff: breaking API change
- ✅
feat(api)!: rename /v1/orders to /v1/checkout BREAKING CHANGE: clients on /v1/orders must migrate to /v1/checkout before 2026-06-01. Old route returns 410 after that date.
Auto-Clarity
Always include body for: breaking changes, security fixes, data migrations, anything reverting a prior commit. Never compress these into subject-only — future debuggers need the context.
Boundaries
Only generates the commit message. Does not run git commit, does not stage files, does not amend. Output the message as a code block ready to paste. "stop caveman-commit" or "normal mode": revert to verbose commit style.
.agents/skills/caveman/SKILL.md
npx skills add sazardev/goca --skill caveman -g -y
SKILL.md
Frontmatter
{
"name": "caveman",
"description": "Ultra-compressed communication mode. Cuts token usage ~75% by speaking like caveman while keeping full technical accuracy. Supports intensity levels: lite, full (default), ultra, wenyan-lite, wenyan-full, wenyan-ultra. Use when user says \"caveman mode\", \"talk like caveman\", \"use caveman\", \"less tokens\", \"be brief\", or invokes \/caveman. Also auto-triggers when token efficiency is requested."
}
Respond terse like smart caveman. All technical substance stay. Only fluff die.
Persistence
ACTIVE EVERY RESPONSE. No revert after many turns. No filler drift. Still active if unsure. Off only: "stop caveman" / "normal mode".
Default: full. Switch: /caveman lite|full|ultra.
Rules
Drop: articles (a/an/the), filler (just/really/basically/actually/simply), pleasantries (sure/certainly/of course/happy to), hedging. Fragments OK. Short synonyms (big not extensive, fix not "implement a solution for"). Technical terms exact. Code blocks unchanged. Errors quoted exact.
Pattern: [thing] [action] [reason]. [next step].
Not: "Sure! I'd be happy to help you with that. The issue you're experiencing is likely caused by..."
Yes: "Bug in auth middleware. Token expiry check use < not <=. Fix:"
Intensity
| Level | What change |
|---|---|
| lite | No filler/hedging. Keep articles + full sentences. Professional but tight |
| full | Drop articles, fragments OK, short synonyms. Classic caveman |
| ultra | Abbreviate prose words (DB/auth/config/req/res/fn/impl), strip conjunctions, arrows for causality (X → Y), one word when one word enough. Code symbols, function names, API names, error strings: never abbreviate |
| wenyan-lite | Semi-classical. Drop filler/hedging but keep grammar structure, classical register |
| wenyan-full | Maximum classical terseness. Fully 文言文. 80-90% character reduction. Classical sentence patterns, verbs precede objects, subjects often omitted, classical particles (之/乃/為/其) |
| wenyan-ultra | Extreme abbreviation while keeping classical Chinese feel. Maximum compression, ultra terse |
Example — "Why React component re-render?"
- lite: "Your component re-renders because you create a new object reference each render. Wrap it in
useMemo." - full: "New object ref each render. Inline object prop = new ref = re-render. Wrap in
useMemo." - ultra: "Inline obj prop → new ref → re-render.
useMemo." - wenyan-lite: "組件頻重繪,以每繪新生對象參照故。以 useMemo 包之。"
- wenyan-full: "物出新參照,致重繪。useMemo .Wrap之。"
- wenyan-ultra: "新參照→重繪。useMemo Wrap。"
Example — "Explain database connection pooling."
- lite: "Connection pooling reuses open connections instead of creating new ones per request. Avoids repeated handshake overhead."
- full: "Pool reuse open DB connections. No new connection per request. Skip handshake overhead."
- ultra: "Pool = reuse DB conn. Skip handshake → fast under load."
- wenyan-full: "池reuse open connection。不每req新開。skip handshake overhead。"
- wenyan-ultra: "池reuse conn。skip handshake → fast。"
Auto-Clarity
Drop caveman when:
- Security warnings
- Irreversible action confirmations
- Multi-step sequences where fragment order or omitted conjunctions risk misread
- Compression itself creates technical ambiguity (e.g.,
"migrate table drop column backup first"— order unclear without articles/conjunctions) - User asks to clarify or repeats question
Resume caveman after clear part done.
Example — destructive op:
Warning: This will permanently delete all rows in the
userstable and cannot be undone.DROP TABLE users;Caveman resume. Verify backup exist first.
Boundaries
Code/commits/PRs: write normal. "stop caveman" or "normal mode": revert. Level persist until changed or session end.
.agents/skills/golang-patterns/SKILL.md
npx skills add sazardev/goca --skill golang-patterns -g -y
SKILL.md
Frontmatter
{
"name": "golang-patterns",
"origin": "ECC",
"description": "Idiomatic Go patterns, best practices, and conventions for building robust, efficient, and maintainable Go applications."
}
Go Development Patterns
Idiomatic Go patterns and best practices for building robust, efficient, and maintainable applications.
When to Activate
- Writing new Go code
- Reviewing Go code
- Refactoring existing Go code
- Designing Go packages/modules
Core Principles
1. Simplicity and Clarity
Go favors simplicity over cleverness. Code should be obvious and easy to read.
// Good: Clear and direct
func GetUser(id string) (*User, error) {
user, err := db.FindUser(id)
if err != nil {
return nil, fmt.Errorf("get user %s: %w", id, err)
}
return user, nil
}
// Bad: Overly clever
func GetUser(id string) (*User, error) {
return func() (*User, error) {
if u, e := db.FindUser(id); e == nil {
return u, nil
} else {
return nil, e
}
}()
}
2. Make the Zero Value Useful
Design types so their zero value is immediately usable without initialization.
// Good: Zero value is useful
type Counter struct {
mu sync.Mutex
count int // zero value is 0, ready to use
}
func (c *Counter) Inc() {
c.mu.Lock()
c.count++
c.mu.Unlock()
}
// Good: bytes.Buffer works with zero value
var buf bytes.Buffer
buf.WriteString("hello")
// Bad: Requires initialization
type BadCounter struct {
counts map[string]int // nil map will panic
}
3. Accept Interfaces, Return Structs
Functions should accept interface parameters and return concrete types.
// Good: Accepts interface, returns concrete type
func ProcessData(r io.Reader) (*Result, error) {
data, err := io.ReadAll(r)
if err != nil {
return nil, err
}
return &Result{Data: data}, nil
}
// Bad: Returns interface (hides implementation details unnecessarily)
func ProcessData(r io.Reader) (io.Reader, error) {
// ...
}
Error Handling Patterns
Error Wrapping with Context
// Good: Wrap errors with context
func LoadConfig(path string) (*Config, error) {
data, err := os.ReadFile(path)
if err != nil {
return nil, fmt.Errorf("load config %s: %w", path, err)
}
var cfg Config
if err := json.Unmarshal(data, &cfg); err != nil {
return nil, fmt.Errorf("parse config %s: %w", path, err)
}
return &cfg, nil
}
Custom Error Types
// Define domain-specific errors
type ValidationError struct {
Field string
Message string
}
func (e *ValidationError) Error() string {
return fmt.Sprintf("validation failed on %s: %s", e.Field, e.Message)
}
// Sentinel errors for common cases
var (
ErrNotFound = errors.New("resource not found")
ErrUnauthorized = errors.New("unauthorized")
ErrInvalidInput = errors.New("invalid input")
)
Error Checking with errors.Is and errors.As
func HandleError(err error) {
// Check for specific error
if errors.Is(err, sql.ErrNoRows) {
log.Println("No records found")
return
}
// Check for error type
var validationErr *ValidationError
if errors.As(err, &validationErr) {
log.Printf("Validation error on field %s: %s",
validationErr.Field, validationErr.Message)
return
}
// Unknown error
log.Printf("Unexpected error: %v", err)
}
Never Ignore Errors
// Bad: Ignoring error with blank identifier
result, _ := doSomething()
// Good: Handle or explicitly document why it's safe to ignore
result, err := doSomething()
if err != nil {
return err
}
// Acceptable: When error truly doesn't matter (rare)
_ = writer.Close() // Best-effort cleanup, error logged elsewhere
Concurrency Patterns
Worker Pool
func WorkerPool(jobs <-chan Job, results chan<- Result, numWorkers int) {
var wg sync.WaitGroup
for i := 0; i < numWorkers; i++ {
wg.Add(1)
go func() {
defer wg.Done()
for job := range jobs {
results <- process(job)
}
}()
}
wg.Wait()
close(results)
}
Context for Cancellation and Timeouts
func FetchWithTimeout(ctx context.Context, url string) ([]byte, error) {
ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
defer cancel()
req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
if err != nil {
return nil, fmt.Errorf("create request: %w", err)
}
resp, err := http.DefaultClient.Do(req)
if err != nil {
return nil, fmt.Errorf("fetch %s: %w", url, err)
}
defer resp.Body.Close()
return io.ReadAll(resp.Body)
}
Graceful Shutdown
func GracefulShutdown(server *http.Server) {
quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
<-quit
log.Println("Shutting down server...")
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
if err := server.Shutdown(ctx); err != nil {
log.Fatalf("Server forced to shutdown: %v", err)
}
log.Println("Server exited")
}
errgroup for Coordinated Goroutines
import "golang.org/x/sync/errgroup"
func FetchAll(ctx context.Context, urls []string) ([][]byte, error) {
g, ctx := errgroup.WithContext(ctx)
results := make([][]byte, len(urls))
for i, url := range urls {
i, url := i, url // Capture loop variables
g.Go(func() error {
data, err := FetchWithTimeout(ctx, url)
if err != nil {
return err
}
results[i] = data
return nil
})
}
if err := g.Wait(); err != nil {
return nil, err
}
return results, nil
}
Avoiding Goroutine Leaks
// Bad: Goroutine leak if context is cancelled
func leakyFetch(ctx context.Context, url string) <-chan []byte {
ch := make(chan []byte)
go func() {
data, _ := fetch(url)
ch <- data // Blocks forever if no receiver
}()
return ch
}
// Good: Properly handles cancellation
func safeFetch(ctx context.Context, url string) <-chan []byte {
ch := make(chan []byte, 1) // Buffered channel
go func() {
data, err := fetch(url)
if err != nil {
return
}
select {
case ch <- data:
case <-ctx.Done():
}
}()
return ch
}
Interface Design
Small, Focused Interfaces
// Good: Single-method interfaces
type Reader interface {
Read(p []byte) (n int, err error)
}
type Writer interface {
Write(p []byte) (n int, err error)
}
type Closer interface {
Close() error
}
// Compose interfaces as needed
type ReadWriteCloser interface {
Reader
Writer
Closer
}
Define Interfaces Where They're Used
// In the consumer package, not the provider
package service
// UserStore defines what this service needs
type UserStore interface {
GetUser(id string) (*User, error)
SaveUser(user *User) error
}
type Service struct {
store UserStore
}
// Concrete implementation can be in another package
// It doesn't need to know about this interface
Optional Behavior with Type Assertions
type Flusher interface {
Flush() error
}
func WriteAndFlush(w io.Writer, data []byte) error {
if _, err := w.Write(data); err != nil {
return err
}
// Flush if supported
if f, ok := w.(Flusher); ok {
return f.Flush()
}
return nil
}
Package Organization
Standard Project Layout
myproject/
├── cmd/
│ └── myapp/
│ └── main.go # Entry point
├── internal/
│ ├── handler/ # HTTP handlers
│ ├── service/ # Business logic
│ ├── repository/ # Data access
│ └── config/ # Configuration
├── pkg/
│ └── client/ # Public API client
├── api/
│ └── v1/ # API definitions (proto, OpenAPI)
├── testdata/ # Test fixtures
├── go.mod
├── go.sum
└── Makefile
Package Naming
// Good: Short, lowercase, no underscores
package http
package json
package user
// Bad: Verbose, mixed case, or redundant
package httpHandler
package json_parser
package userService // Redundant 'Service' suffix
Avoid Package-Level State
// Bad: Global mutable state
var db *sql.DB
func init() {
db, _ = sql.Open("postgres", os.Getenv("DATABASE_URL"))
}
// Good: Dependency injection
type Server struct {
db *sql.DB
}
func NewServer(db *sql.DB) *Server {
return &Server{db: db}
}
Struct Design
Functional Options Pattern
type Server struct {
addr string
timeout time.Duration
logger *log.Logger
}
type Option func(*Server)
func WithTimeout(d time.Duration) Option {
return func(s *Server) {
s.timeout = d
}
}
func WithLogger(l *log.Logger) Option {
return func(s *Server) {
s.logger = l
}
}
func NewServer(addr string, opts ...Option) *Server {
s := &Server{
addr: addr,
timeout: 30 * time.Second, // default
logger: log.Default(), // default
}
for _, opt := range opts {
opt(s)
}
return s
}
// Usage
server := NewServer(":8080",
WithTimeout(60*time.Second),
WithLogger(customLogger),
)
Embedding for Composition
type Logger struct {
prefix string
}
func (l *Logger) Log(msg string) {
fmt.Printf("[%s] %s\n", l.prefix, msg)
}
type Server struct {
*Logger // Embedding - Server gets Log method
addr string
}
func NewServer(addr string) *Server {
return &Server{
Logger: &Logger{prefix: "SERVER"},
addr: addr,
}
}
// Usage
s := NewServer(":8080")
s.Log("Starting...") // Calls embedded Logger.Log
Memory and Performance
Preallocate Slices When Size is Known
// Bad: Grows slice multiple times
func processItems(items []Item) []Result {
var results []Result
for _, item := range items {
results = append(results, process(item))
}
return results
}
// Good: Single allocation
func processItems(items []Item) []Result {
results := make([]Result, 0, len(items))
for _, item := range items {
results = append(results, process(item))
}
return results
}
Use sync.Pool for Frequent Allocations
var bufferPool = sync.Pool{
New: func() interface{} {
return new(bytes.Buffer)
},
}
func ProcessRequest(data []byte) []byte {
buf := bufferPool.Get().(*bytes.Buffer)
defer func() {
buf.Reset()
bufferPool.Put(buf)
}()
buf.Write(data)
// Process...
return buf.Bytes()
}
Avoid String Concatenation in Loops
// Bad: Creates many string allocations
func join(parts []string) string {
var result string
for _, p := range parts {
result += p + ","
}
return result
}
// Good: Single allocation with strings.Builder
func join(parts []string) string {
var sb strings.Builder
for i, p := range parts {
if i > 0 {
sb.WriteString(",")
}
sb.WriteString(p)
}
return sb.String()
}
// Best: Use standard library
func join(parts []string) string {
return strings.Join(parts, ",")
}
Go Tooling Integration
Essential Commands
# Build and run
go build ./...
go run ./cmd/myapp
# Testing
go test ./...
go test -race ./...
go test -cover ./...
# Static analysis
go vet ./...
staticcheck ./...
golangci-lint run
# Module management
go mod tidy
go mod verify
# Formatting
gofmt -w .
goimports -w .
Recommended Linter Configuration (.golangci.yml)
linters:
enable:
- errcheck
- gosimple
- govet
- ineffassign
- staticcheck
- unused
- gofmt
- goimports
- misspell
- unconvert
- unparam
linters-settings:
errcheck:
check-type-assertions: true
govet:
check-shadowing: true
issues:
exclude-use-default: false
Quick Reference: Go Idioms
| Idiom | Description |
|---|---|
| Accept interfaces, return structs | Functions accept interface params, return concrete types |
| Errors are values | Treat errors as first-class values, not exceptions |
| Don't communicate by sharing memory | Use channels for coordination between goroutines |
| Make the zero value useful | Types should work without explicit initialization |
| A little copying is better than a little dependency | Avoid unnecessary external dependencies |
| Clear is better than clever | Prioritize readability over cleverness |
| gofmt is no one's favorite but everyone's friend | Always format with gofmt/goimports |
| Return early | Handle errors first, keep happy path unindented |
Anti-Patterns to Avoid
// Bad: Naked returns in long functions
func process() (result int, err error) {
// ... 50 lines ...
return // What is being returned?
}
// Bad: Using panic for control flow
func GetUser(id string) *User {
user, err := db.Find(id)
if err != nil {
panic(err) // Don't do this
}
return user
}
// Bad: Passing context in struct
type Request struct {
ctx context.Context // Context should be first param
ID string
}
// Good: Context as first parameter
func ProcessRequest(ctx context.Context, id string) error {
// ...
}
// Bad: Mixing value and pointer receivers
type Counter struct{ n int }
func (c Counter) Value() int { return c.n } // Value receiver
func (c *Counter) Increment() { c.n++ } // Pointer receiver
// Pick one style and be consistent
Remember: Go code should be boring in the best way - predictable, consistent, and easy to understand. When in doubt, keep it simple.
.agents/skills/golang-testing/SKILL.md
npx skills add sazardev/goca --skill golang-testing -g -y
SKILL.md
Frontmatter
{
"name": "golang-testing",
"origin": "ECC",
"description": "Go testing patterns including table-driven tests, subtests, benchmarks, fuzzing, and test coverage. Follows TDD methodology with idiomatic Go practices."
}
Go Testing Patterns
Comprehensive Go testing patterns for writing reliable, maintainable tests following TDD methodology.
When to Activate
- Writing new Go functions or methods
- Adding test coverage to existing code
- Creating benchmarks for performance-critical code
- Implementing fuzz tests for input validation
- Following TDD workflow in Go projects
TDD Workflow for Go
The RED-GREEN-REFACTOR Cycle
RED → Write a failing test first
GREEN → Write minimal code to pass the test
REFACTOR → Improve code while keeping tests green
REPEAT → Continue with next requirement
Step-by-Step TDD in Go
// Step 1: Define the interface/signature
// calculator.go
package calculator
func Add(a, b int) int {
panic("not implemented") // Placeholder
}
// Step 2: Write failing test (RED)
// calculator_test.go
package calculator
import "testing"
func TestAdd(t *testing.T) {
got := Add(2, 3)
want := 5
if got != want {
t.Errorf("Add(2, 3) = %d; want %d", got, want)
}
}
// Step 3: Run test - verify FAIL
// $ go test
// --- FAIL: TestAdd (0.00s)
// panic: not implemented
// Step 4: Implement minimal code (GREEN)
func Add(a, b int) int {
return a + b
}
// Step 5: Run test - verify PASS
// $ go test
// PASS
// Step 6: Refactor if needed, verify tests still pass
Table-Driven Tests
The standard pattern for Go tests. Enables comprehensive coverage with minimal code.
func TestAdd(t *testing.T) {
tests := []struct {
name string
a, b int
expected int
}{
{"positive numbers", 2, 3, 5},
{"negative numbers", -1, -2, -3},
{"zero values", 0, 0, 0},
{"mixed signs", -1, 1, 0},
{"large numbers", 1000000, 2000000, 3000000},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := Add(tt.a, tt.b)
if got != tt.expected {
t.Errorf("Add(%d, %d) = %d; want %d",
tt.a, tt.b, got, tt.expected)
}
})
}
}
Table-Driven Tests with Error Cases
func TestParseConfig(t *testing.T) {
tests := []struct {
name string
input string
want *Config
wantErr bool
}{
{
name: "valid config",
input: `{"host": "localhost", "port": 8080}`,
want: &Config{Host: "localhost", Port: 8080},
},
{
name: "invalid JSON",
input: `{invalid}`,
wantErr: true,
},
{
name: "empty input",
input: "",
wantErr: true,
},
{
name: "minimal config",
input: `{}`,
want: &Config{}, // Zero value config
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := ParseConfig(tt.input)
if tt.wantErr {
if err == nil {
t.Error("expected error, got nil")
}
return
}
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("got %+v; want %+v", got, tt.want)
}
})
}
}
Subtests and Sub-benchmarks
Organizing Related Tests
func TestUser(t *testing.T) {
// Setup shared by all subtests
db := setupTestDB(t)
t.Run("Create", func(t *testing.T) {
user := &User{Name: "Alice"}
err := db.CreateUser(user)
if err != nil {
t.Fatalf("CreateUser failed: %v", err)
}
if user.ID == "" {
t.Error("expected user ID to be set")
}
})
t.Run("Get", func(t *testing.T) {
user, err := db.GetUser("alice-id")
if err != nil {
t.Fatalf("GetUser failed: %v", err)
}
if user.Name != "Alice" {
t.Errorf("got name %q; want %q", user.Name, "Alice")
}
})
t.Run("Update", func(t *testing.T) {
// ...
})
t.Run("Delete", func(t *testing.T) {
// ...
})
}
Parallel Subtests
func TestParallel(t *testing.T) {
tests := []struct {
name string
input string
}{
{"case1", "input1"},
{"case2", "input2"},
{"case3", "input3"},
}
for _, tt := range tests {
tt := tt // Capture range variable
t.Run(tt.name, func(t *testing.T) {
t.Parallel() // Run subtests in parallel
result := Process(tt.input)
// assertions...
_ = result
})
}
}
Test Helpers
Helper Functions
func setupTestDB(t *testing.T) *sql.DB {
t.Helper() // Marks this as a helper function
db, err := sql.Open("sqlite3", ":memory:")
if err != nil {
t.Fatalf("failed to open database: %v", err)
}
// Cleanup when test finishes
t.Cleanup(func() {
db.Close()
})
// Run migrations
if _, err := db.Exec(schema); err != nil {
t.Fatalf("failed to create schema: %v", err)
}
return db
}
func assertNoError(t *testing.T, err error) {
t.Helper()
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
}
func assertEqual[T comparable](t *testing.T, got, want T) {
t.Helper()
if got != want {
t.Errorf("got %v; want %v", got, want)
}
}
Temporary Files and Directories
func TestFileProcessing(t *testing.T) {
// Create temp directory - automatically cleaned up
tmpDir := t.TempDir()
// Create test file
testFile := filepath.Join(tmpDir, "test.txt")
err := os.WriteFile(testFile, []byte("test content"), 0644)
if err != nil {
t.Fatalf("failed to create test file: %v", err)
}
// Run test
result, err := ProcessFile(testFile)
if err != nil {
t.Fatalf("ProcessFile failed: %v", err)
}
// Assert...
_ = result
}
Golden Files
Testing against expected output files stored in testdata/.
var update = flag.Bool("update", false, "update golden files")
func TestRender(t *testing.T) {
tests := []struct {
name string
input Template
}{
{"simple", Template{Name: "test"}},
{"complex", Template{Name: "test", Items: []string{"a", "b"}}},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := Render(tt.input)
golden := filepath.Join("testdata", tt.name+".golden")
if *update {
// Update golden file: go test -update
err := os.WriteFile(golden, got, 0644)
if err != nil {
t.Fatalf("failed to update golden file: %v", err)
}
}
want, err := os.ReadFile(golden)
if err != nil {
t.Fatalf("failed to read golden file: %v", err)
}
if !bytes.Equal(got, want) {
t.Errorf("output mismatch:\ngot:\n%s\nwant:\n%s", got, want)
}
})
}
}
Mocking with Interfaces
Interface-Based Mocking
// Define interface for dependencies
type UserRepository interface {
GetUser(id string) (*User, error)
SaveUser(user *User) error
}
// Production implementation
type PostgresUserRepository struct {
db *sql.DB
}
func (r *PostgresUserRepository) GetUser(id string) (*User, error) {
// Real database query
}
// Mock implementation for tests
type MockUserRepository struct {
GetUserFunc func(id string) (*User, error)
SaveUserFunc func(user *User) error
}
func (m *MockUserRepository) GetUser(id string) (*User, error) {
return m.GetUserFunc(id)
}
func (m *MockUserRepository) SaveUser(user *User) error {
return m.SaveUserFunc(user)
}
// Test using mock
func TestUserService(t *testing.T) {
mock := &MockUserRepository{
GetUserFunc: func(id string) (*User, error) {
if id == "123" {
return &User{ID: "123", Name: "Alice"}, nil
}
return nil, ErrNotFound
},
}
service := NewUserService(mock)
user, err := service.GetUserProfile("123")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if user.Name != "Alice" {
t.Errorf("got name %q; want %q", user.Name, "Alice")
}
}
Benchmarks
Basic Benchmarks
func BenchmarkProcess(b *testing.B) {
data := generateTestData(1000)
b.ResetTimer() // Don't count setup time
for i := 0; i < b.N; i++ {
Process(data)
}
}
// Run: go test -bench=BenchmarkProcess -benchmem
// Output: BenchmarkProcess-8 10000 105234 ns/op 4096 B/op 10 allocs/op
Benchmark with Different Sizes
func BenchmarkSort(b *testing.B) {
sizes := []int{100, 1000, 10000, 100000}
for _, size := range sizes {
b.Run(fmt.Sprintf("size=%d", size), func(b *testing.B) {
data := generateRandomSlice(size)
b.ResetTimer()
for i := 0; i < b.N; i++ {
// Make a copy to avoid sorting already sorted data
tmp := make([]int, len(data))
copy(tmp, data)
sort.Ints(tmp)
}
})
}
}
Memory Allocation Benchmarks
func BenchmarkStringConcat(b *testing.B) {
parts := []string{"hello", "world", "foo", "bar", "baz"}
b.Run("plus", func(b *testing.B) {
for i := 0; i < b.N; i++ {
var s string
for _, p := range parts {
s += p
}
_ = s
}
})
b.Run("builder", func(b *testing.B) {
for i := 0; i < b.N; i++ {
var sb strings.Builder
for _, p := range parts {
sb.WriteString(p)
}
_ = sb.String()
}
})
b.Run("join", func(b *testing.B) {
for i := 0; i < b.N; i++ {
_ = strings.Join(parts, "")
}
})
}
Fuzzing (Go 1.18+)
Basic Fuzz Test
func FuzzParseJSON(f *testing.F) {
// Add seed corpus
f.Add(`{"name": "test"}`)
f.Add(`{"count": 123}`)
f.Add(`[]`)
f.Add(`""`)
f.Fuzz(func(t *testing.T, input string) {
var result map[string]interface{}
err := json.Unmarshal([]byte(input), &result)
if err != nil {
// Invalid JSON is expected for random input
return
}
// If parsing succeeded, re-encoding should work
_, err = json.Marshal(result)
if err != nil {
t.Errorf("Marshal failed after successful Unmarshal: %v", err)
}
})
}
// Run: go test -fuzz=FuzzParseJSON -fuzztime=30s
Fuzz Test with Multiple Inputs
func FuzzCompare(f *testing.F) {
f.Add("hello", "world")
f.Add("", "")
f.Add("abc", "abc")
f.Fuzz(func(t *testing.T, a, b string) {
result := Compare(a, b)
// Property: Compare(a, a) should always equal 0
if a == b && result != 0 {
t.Errorf("Compare(%q, %q) = %d; want 0", a, b, result)
}
// Property: Compare(a, b) and Compare(b, a) should have opposite signs
reverse := Compare(b, a)
if (result > 0 && reverse >= 0) || (result < 0 && reverse <= 0) {
if result != 0 || reverse != 0 {
t.Errorf("Compare(%q, %q) = %d, Compare(%q, %q) = %d; inconsistent",
a, b, result, b, a, reverse)
}
}
})
}
Test Coverage
Running Coverage
# Basic coverage
go test -cover ./...
# Generate coverage profile
go test -coverprofile=coverage.out ./...
# View coverage in browser
go tool cover -html=coverage.out
# View coverage by function
go tool cover -func=coverage.out
# Coverage with race detection
go test -race -coverprofile=coverage.out ./...
Coverage Targets
| Code Type | Target |
|---|---|
| Critical business logic | 100% |
| Public APIs | 90%+ |
| General code | 80%+ |
| Generated code | Exclude |
Excluding Generated Code from Coverage
//go:generate mockgen -source=interface.go -destination=mock_interface.go
// In coverage profile, exclude with build tags:
// go test -cover -tags=!generate ./...
HTTP Handler Testing
func TestHealthHandler(t *testing.T) {
// Create request
req := httptest.NewRequest(http.MethodGet, "/health", nil)
w := httptest.NewRecorder()
// Call handler
HealthHandler(w, req)
// Check response
resp := w.Result()
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
t.Errorf("got status %d; want %d", resp.StatusCode, http.StatusOK)
}
body, _ := io.ReadAll(resp.Body)
if string(body) != "OK" {
t.Errorf("got body %q; want %q", body, "OK")
}
}
func TestAPIHandler(t *testing.T) {
tests := []struct {
name string
method string
path string
body string
wantStatus int
wantBody string
}{
{
name: "get user",
method: http.MethodGet,
path: "/users/123",
wantStatus: http.StatusOK,
wantBody: `{"id":"123","name":"Alice"}`,
},
{
name: "not found",
method: http.MethodGet,
path: "/users/999",
wantStatus: http.StatusNotFound,
},
{
name: "create user",
method: http.MethodPost,
path: "/users",
body: `{"name":"Bob"}`,
wantStatus: http.StatusCreated,
},
}
handler := NewAPIHandler()
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
var body io.Reader
if tt.body != "" {
body = strings.NewReader(tt.body)
}
req := httptest.NewRequest(tt.method, tt.path, body)
req.Header.Set("Content-Type", "application/json")
w := httptest.NewRecorder()
handler.ServeHTTP(w, req)
if w.Code != tt.wantStatus {
t.Errorf("got status %d; want %d", w.Code, tt.wantStatus)
}
if tt.wantBody != "" && w.Body.String() != tt.wantBody {
t.Errorf("got body %q; want %q", w.Body.String(), tt.wantBody)
}
})
}
}
Testing Commands
# Run all tests
go test ./...
# Run tests with verbose output
go test -v ./...
# Run specific test
go test -run TestAdd ./...
# Run tests matching pattern
go test -run "TestUser/Create" ./...
# Run tests with race detector
go test -race ./...
# Run tests with coverage
go test -cover -coverprofile=coverage.out ./...
# Run short tests only
go test -short ./...
# Run tests with timeout
go test -timeout 30s ./...
# Run benchmarks
go test -bench=. -benchmem ./...
# Run fuzzing
go test -fuzz=FuzzParse -fuzztime=30s ./...
# Count test runs (for flaky test detection)
go test -count=10 ./...
Best Practices
DO:
- Write tests FIRST (TDD)
- Use table-driven tests for comprehensive coverage
- Test behavior, not implementation
- Use
t.Helper()in helper functions - Use
t.Parallel()for independent tests - Clean up resources with
t.Cleanup() - Use meaningful test names that describe the scenario
DON'T:
- Test private functions directly (test through public API)
- Use
time.Sleep()in tests (use channels or conditions) - Ignore flaky tests (fix or remove them)
- Mock everything (prefer integration tests when possible)
- Skip error path testing
Integration with CI/CD
# GitHub Actions example
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version: '1.22'
- name: Run tests
run: go test -race -coverprofile=coverage.out ./...
- name: Check coverage
run: |
go tool cover -func=coverage.out | grep total | awk '{print $3}' | \
awk -F'%' '{if ($1 < 80) exit 1}'
Remember: Tests are documentation. They show how your code is meant to be used. Write them clearly and keep them up to date.
skills/ddd-teaching-skill/SKILL.md
npx skills add sazardev/goca --skill ddd-teaching-skill -g -y
SKILL.md
Frontmatter
{
"name": "ddd-teaching-skill",
"description": "Enseña Domain-Driven Design y Clean Architecture de forma guiada usando Goca CLI. Actívar cuando el usuario pregunte \"aprender DDD\", \"como usar Goca\", \"enseñame Clean Architecture\", \"que es domain\", \"como generar entity\/usecase\/repository\/handler\", o pida practicar DDD con Goca. Inactívar al completar el flujo o si el usuario cambia a tareas no relacionadas con aprendizaje."
}
DDD Teaching — Aprendizaje Guiado con Goca
Filosofía
DDD + Clean Architecture resuelven el problema de código que muere lentamente: lógica de negocio mezclada con frameworks, tests imposibles, cambios que cascada. La solución es la Dependency Rule — las dependencias apuntan hacia adentro.
Goca materializa esta arquitectura generando código en 4 capas:
Handler (adapter) → UseCase (app logic) → Repository (persistence) → Domain (pure)
↓ ↓ ↓ ↓
HTTP/gRPC business logic SQL/Redis entities
Cada capa solo conoce la que está inmediatamente dentro de ella, y siempre a través de interfaces.
Activación
Invocar este skill automáticamente cuando el usuario:
- Pregunte conceptos de DDD o Clean Architecture
- Quiera aprender a usar Goca paso a paso
- Pida "generar un feature completo" sin entender qué genera
- Muestre código con violaciones arquitectónicas (handler con GORM, usecase con http, etc.)
Mapa Conceptual ↔ Comandos Goca
| Concepto DDD | Comando Goca | Archivo generado | Propósito |
|---|---|---|---|
| Entidad | goca entity |
internal/domain/product.go |
Pureza del dominio, invariantes |
| Value Object | goca entity --validation |
domain/product.go — Validate() |
Validación encapsulada |
| Repository interface | goca repository --interface-only |
repository/interfaces.go |
Contrato de persistencia |
| Repository impl | goca repository -d postgres |
repository/postgres_product.go |
GORM encapsulado |
| DTO / Application Service | goca usecase |
usecase/dto.go, service.go |
Separación capas |
| UseCase interface | goca usecase |
usecase/product_usecase.go |
Contrato para handlers |
| Handler / Adapter | goca handler -t http |
handler/http/product_handler.go |
Delivery |
| DI Container | goca di |
di/container.go |
Wiring |
Flujo Guiado — 6 Pasos (con TDD)
Cada paso: Concepto DDD → Comando Goca → Código generado → Test
Paso 1: Init — Scaffold del Proyecto
Concepto: Separación en capas limpias. El directorio internal/ es la frontera — nada fuera de internal/ puede importar lo de adentro, pero lo de adentro sí puede importar pkg/.
Comando:
goca init ecommerce --module github.com/myapp/ecommerce --database postgres
Estructura generada:
internal/
domain/ ← Puro: sin imports externos, solo lógica de negocio
usecase/ ← Solo importa domain + repository interfaces
repository/ ← Implementa interfaces; conoce GORM, no el usecase
handler/ ← Conoce interfaces de usecase, nunca repository directo
Análisis: Cada carpeta corresponde exactamente a un círculo de Clean Architecture. La regla de dependencia se aplica a nivel de import: handler → usecase → repository → domain. Prohibido saltar capas.
Test:
cd ecommerce && go build ./... && go vet ./...
# Debe pasar sin errores: es el esqueleto vacío pero correcto
Paso 2: Entity — Domain Puro
Concepto: Una entidad DDD tiene identidad (ID) e invariantes — reglas que siempre deben cumplirse. Los Value Objects son inmutables y se comparan por valor. Ambos viven en domain/ sin dependencias externas.
Comando:
goca entity Product --fields "name:string,price:float64,category:string" --validation --business-rules
Código generado (extraído de cmd/templates.go y cmd/template_components.go):
// internal/domain/product.go
package domain
type Product struct {
ID int `json:"id" gorm:"primaryKey"`
Name string `json:"name" gorm:"type:varchar(255);not null"`
Price float64 `json:"price" gorm:"type:decimal(10,2);not null"`
Category string `json:"category" gorm:"type:varchar(100)"`
}
func (p *Product) Validate() error {
if p.Name == "" {
return errors.New("product name is required")
}
if p.Price <= 0 {
return errors.New("product price must be positive")
}
return nil
}
func (p *Product) IsExpensive() bool {
return p.Price > 1000
}
// internal/domain/errors.go
package domain
import "errors"
var (
ErrInvalidProductName = errors.New("product name is required")
ErrInvalidProductPrice = errors.New("product price must be positive")
)
Análisis:
- Struct
Product: sin lógica externa. Los tags de campo son metadata de serialización, no comportamiento. Validate(): invariante expresado como método del dominio. Sin dependencias externas. Sin ORM. Sin HTTP.IsExpensive(): regla de negocio co-localizada con los datos. Cambia la regla? Cambias este método, no un service remoto.- Errores como sentinel values (
var Err...), no strings mágicos. El consumidor puede hacererrors.Is(err, domain.ErrInvalidProductName).
Test (TDD primero):
// internal/domain/product_test.go
func TestProduct_Validate(t *testing.T) {
tests := []struct {
name string
product Product
wantErr bool
}{
{"valid product", Product{Name: "Laptop", Price: 999.99, Category: "Electronics"}, false},
{"empty name", Product{Price: 999.99}, true},
{"zero price", Product{Name: "Laptop", Price: 0}, true},
{"negative price", Product{Name: "Laptop", Price: -1}, true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := tt.product.Validate()
if (err != nil) != tt.wantErr {
t.Errorf("Validate() error = %v, wantErr = %v", err, tt.wantErr)
}
})
}
}
func TestProduct_IsExpensive(t *testing.T) {
cheap := Product{Name: "Notebook", Price: 10}
expensive := Product{Name: "MacBook", Price: 2500}
if cheap.IsExpensive() {
t.Error("expected cheap product to not be expensive")
}
if !expensive.IsExpensive() {
t.Error("expected expensive product to be expensive")
}
}
Paso 3: UseCase — Lógica de Aplicación
Concepto: El Application Service orquesta la lógica de negocio. No contiene reglas de dominio (esas van en la entidad). Usa DTOs para desacoplar el mundo exterior del dominio. Depende de interfaces de repositorio, no de implementaciones concretas.
Comando:
goca usecase ProductService --entity Product --operations "create,read,update,delete,list"
Código generado (extraído de cmd/template_components.go + cmd/templates.go):
// internal/usecase/product_usecase.go
package usecase
import (
"github.com/myapp/ecommerce/internal/domain"
"github.com/myapp/ecommerce/internal/repository"
)
type ProductUseCase interface {
CreateProduct(input CreateProductInput) (*CreateProductOutput, error)
GetProductByID(id int) (*ProductOutput, error)
UpdateProduct(id int, input UpdateProductInput) error
DeleteProduct(id int) error
ListProducts() (*ListProductsOutput, error)
}
type productService struct {
repo repository.ProductRepository
}
func NewProductService(repo repository.ProductRepository) ProductUseCase {
return &productService{repo: repo}
}
func (s *productService) CreateProduct(input CreateProductInput) (*CreateProductOutput, error) {
product := domain.Product{
Name: input.Name,
Price: input.Price,
Category: input.Category,
}
if err := product.Validate(); err != nil {
return nil, err
}
if err := s.repo.Save(&product); err != nil {
return nil, err
}
return &CreateProductOutput{
Product: &product,
Message: "Product created successfully",
}, nil
}
// internal/usecase/dto.go
type CreateProductInput struct {
Name string `json:"name"`
Price float64 `json:"price"`
Category string `json:"category"`
}
type CreateProductOutput struct {
Product *domain.Product `json:"product"`
Message string `json:"message"`
}
type UpdateProductInput struct {
Name *string `json:"name,omitempty"`
Price *float64 `json:"price,omitempty"`
Category *string `json:"category,omitempty"`
}
type ProductOutput struct {
Product *domain.Product `json:"product"`
}
type ListProductsOutput struct {
Products []domain.Product `json:"products"`
Total int `json:"total"`
}
// internal/usecase/interfaces.go
package usecase
import "github.com/myapp/ecommerce/internal/domain"
type ProductRepository interface {
Save(product *domain.Product) error
FindByID(id int) (*domain.Product, error)
Update(product *domain.Product) error
Delete(id int) error
FindAll() ([]domain.Product, error)
}
Análisis:
ProductUseCasees interfaz pública. El handler programa contra esta interfaz, no contra el struct concreto.productServicees privado (minúscula). Solo se exporta el constructorNewProductService(repo). Nadie puede acoplar al tipo concreto.- Constructor injection: el repo se inyecta. El service no crea su propio repo, no llama a
sql.Open(), no sabe si es Postgres o Mock. CreateProductInputvsdomain.Product: el input puede tener validaciones distintas al domain. El Update usa punteros (*string) para distinguir "no enviado" de "enviado vacío".- La interfaz
ProductRepositoryenusecase/interfaces.goes la misma que enrepository/interfaces.go. El usecase la necesita para su firma.
Test con Mock (TDD — RED antes de implementar):
// internal/usecase/product_service_test.go
func TestCreateProduct_Success(t *testing.T) {
mockRepo := new(MockProductRepository)
mockRepo.On("Save", mock.AnythingOfType("*domain.Product")).Return(nil)
svc := NewProductService(mockRepo)
input := CreateProductInput{Name: "Laptop", Price: 999.99, Category: "Electronics"}
output, err := svc.CreateProduct(input)
assert.NoError(t, err)
assert.NotNil(t, output.Product)
assert.Equal(t, "Laptop", output.Product.Name)
assert.Equal(t, "Product created successfully", output.Message)
mockRepo.AssertExpectations(t)
}
func TestCreateProduct_InvalidData(t *testing.T) {
mockRepo := new(MockProductRepository)
svc := NewProductService(mockRepo)
input := CreateProductInput{Name: "", Price: 0}
_, err := svc.CreateProduct(input)
assert.Error(t, err)
mockRepo.AssertNotCalled(t, "Save")
}
Paso 4: Repository — Persistencia
Concepto: El Repository Pattern abstrae el almacenamiento detrás de una interfaz. El dominio y el usecase conocen la interfaz, no la implementación. GORM, SQL, Redis, MongoDB — todo queda encapsulado detrás de esta interfaz.
Comando:
goca repository Product --database postgres --transactions
Código generado (extraído de cmd/templates.go + cmd/repository.go):
// internal/repository/interfaces.go
package repository
import "github.com/myapp/ecommerce/internal/domain"
type ProductRepository interface {
Save(product *domain.Product) error
FindByID(id int) (*domain.Product, error)
Update(product *domain.Product) error
Delete(id int) error
FindAll() ([]domain.Product, error)
}
// internal/repository/postgres_product_repository.go
package repository
import (
"gorm.io/gorm"
"github.com/myapp/ecommerce/internal/domain"
)
type postgresProductRepository struct {
db *gorm.DB
}
func NewPostgresProductRepository(db *gorm.DB) ProductRepository {
return &postgresProductRepository{db: db}
}
func (r *postgresProductRepository) Save(product *domain.Product) error {
return r.db.Create(product).Error
}
func (r *postgresProductRepository) FindByID(id int) (*domain.Product, error) {
var product domain.Product
if err := r.db.First(&product, id).Error; err != nil {
return nil, err
}
return &product, nil
}
func (r *postgresProductRepository) Update(product *domain.Product) error {
return r.db.Save(product).Error
}
func (r *postgresProductRepository) Delete(id int) error {
return r.db.Delete(&domain.Product{}, id).Error
}
func (r *postgresProductRepository) FindAll() ([]domain.Product, error) {
var products []domain.Product
if err := r.db.Find(&products).Error; err != nil {
return nil, err
}
return products, nil
}
Análisis:
- Interfaz en
repository/interfaces.godefine el contrato. El usecase la consume. El handler ni la conoce. postgresProductRepositoryes privado — nadie fuera del package la instancia excepto el DI container.- El constructor
NewPostgresProductRepository(db *gorm.DB) ProductRepositorydevuelve la interfaz, no el struct. - GORM está completamente encapsulado en este package. Si migras a MongoDB, cambias este archivo, no tocas domain ni usecase.
--transactionsagregaSaveWithTx(tx *gorm.DB, ...)para Unit of Work.
Paso 5: Handler — Delivery Adapter
Concepto: El Adapter convierte requests externos (HTTP, gRPC, CLI) en llamadas a usecase. No contiene lógica de negocio. Solo serialización, routing, delegación.
Comando:
goca handler Product --type http --validation
Código generado (extraído de cmd/templates.go):
// internal/handler/http/product_handler.go
package http
import (
"encoding/json"
"net/http"
"strconv"
"github.com/gorilla/mux"
"github.com/myapp/ecommerce/internal/usecase"
)
type ProductHandler struct {
usecase usecase.ProductUseCase
}
func NewProductHandler(uc usecase.ProductUseCase) *ProductHandler {
return &ProductHandler{usecase: uc}
}
func (h *ProductHandler) CreateProduct(w http.ResponseWriter, r *http.Request) {
var input usecase.CreateProductInput
if err := json.NewDecoder(r.Body).Decode(&input); err != nil {
http.Error(w, "Invalid request body", http.StatusBadRequest)
return
}
output, err := h.usecase.CreateProduct(input)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusCreated)
json.NewEncoder(w).Encode(output)
}
func (h *ProductHandler) GetProduct(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
id, err := strconv.Atoi(vars["id"])
if err != nil {
http.Error(w, "Invalid product ID", http.StatusBadRequest)
return
}
output, err := h.usecase.GetProductByID(id)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(output)
}
// internal/handler/http/routes.go
package http
import (
"github.com/gorilla/mux"
"github.com/myapp/ecommerce/internal/usecase"
)
func SetupProductRoutes(router *mux.Router, uc usecase.ProductUseCase) {
handler := NewProductHandler(uc)
router.HandleFunc("/products", handler.CreateProduct).Methods("POST")
router.HandleFunc("/products/{id}", handler.GetProduct).Methods("GET")
router.HandleFunc("/products/{id}", handler.UpdateProduct).Methods("PUT")
router.HandleFunc("/products/{id}", handler.DeleteProduct).Methods("DELETE")
router.HandleFunc("/products", handler.ListProducts).Methods("GET")
}
Análisis:
- Handler importa
usecasesolo. No importarepository, no importagorm. ✅ ProductHandlerrecibeusecase.ProductUseCase(interfaz), no el service concreto.- No hay lógica de negocio aquí. Solo: parse request → call usecase → serialize response.
- Si cambias HTTP → gRPC, el handler cambia pero
usecaseydomainno se tocan. Routesse inyecta el usecase y construye el handler — el router no necesita saber cómo construirlo.
Test con httptest:
func TestCreateProductHandler(t *testing.T) {
mockUC := new(MockProductUseCase)
mockUC.On("CreateProduct", mock.Anything).Return(&usecase.CreateProductOutput{
Product: &domain.Product{Name: "Laptop", Price: 999.99},
Message: "Product created successfully",
}, nil)
handler := NewProductHandler(mockUC)
body := `{"name":"Laptop","price":999.99,"category":"Electronics"}`
req := httptest.NewRequest(http.MethodPost, "/products", strings.NewReader(body))
req.Header.Set("Content-Type", "application/json")
w := httptest.NewRecorder()
handler.CreateProduct(w, req)
assert.Equal(t, http.StatusCreated, w.Code)
assert.Contains(t, w.Body.String(), "Laptop")
mockUC.AssertExpectations(t)
}
Paso 6: DI — Wiring Completo
Concepto: El Composition Root es el único lugar donde se instancian implementaciones concretas. Construye el grafo de objetos completo. Todas las demás capas reciben sus dependencias ya construidas.
Comando:
goca di --features "Product" --database postgres
Código generado (extraído de cmd/di.go):
// internal/di/container.go
package di
import (
"gorm.io/gorm"
"github.com/myapp/ecommerce/internal/repository"
"github.com/myapp/ecommerce/internal/usecase"
"github.com/myapp/ecommerce/internal/handler/http"
)
type Container struct {
db *gorm.DB
productRepo repository.ProductRepository
productUC usecase.ProductUseCase
productHandler *http.ProductHandler
}
func NewContainer(db *gorm.DB) *Container {
c := &Container{db: db}
c.setupRepositories()
c.setupUseCases()
c.setupHandlers()
return c
}
func (c *Container) setupRepositories() {
c.productRepo = repository.NewPostgresProductRepository(c.db)
}
func (c *Container) setupUseCases() {
c.productUC = usecase.NewProductService(c.productRepo)
}
func (c *Container) setupHandlers() {
c.productHandler = http.NewProductHandler(c.productUC)
}
func (c *Container) ProductHandler() *http.ProductHandler {
return c.productHandler
}
Análisis:
NewContainer(db)recibe la conexión a BD. El container construye TODO el grafo.- Único lugar donde se llama a
NewPostgresProductRepository,NewProductService,NewProductHandler. - Si quieres cambiar Postgres → MySQL, cambias UNA línea en
setupRepositories. - Los getters (
ProductHandler()) exponen solo lo quemain.gonecesita.
Feature Completo (atajo)
goca feature Product --fields "name:string,price:float64,category:string" --database postgres --validation --handlers http
Este comando ejecuta Pasos 2-6 en una sola operación. Equivale a:
1. goca entity Product --fields "name:string,price:float64,category:string" --validation --business-rules
2. goca usecase ProductService --entity Product --operations "create,read,update,delete,list"
3. goca repository Product --database postgres
4. goca handler Product --type http --validation
5. goca messages Product --all
6. goca di --features "Product" --database postgres
Además integra el handler en main.go y registra migraciones automáticas.
Anti-Patterns (qué NO hacer)
❌ Handler importando repository directamente
type ProductHandler struct {
repo *gorm.DB // MAL: handler conoce la BD y GORM!
}
✅ Handler solo conoce usecase.ProductUseCase
❌ UseCase con GORM o SQL
type productService struct {
db *gorm.DB // MAL: lógica de negocio conoce GORM
}
✅ UseCase recibe repository.ProductRepository (interfaz abstracta)
❌ Entidad anémica (solo getters/setters, sin comportamiento)
type Product struct {
Name string // MAL: sin Validate(), sin reglas de negocio
Price float64 // solo es una struct de datos, no una entidad DDD
}
✅ Domain tiene métodos: Validate(), IsExpensive(), etc.
❌ DTOs expuestos desde handler — usa usecase.CreateProductInput, no crees DTOs en handler.
❌ init() y variables globales — usa constructor injection siempre.
Límites del Skill
Este skill enseña DDD + Clean Architecture a través de Goca. Para otros aspectos, delegar:
| Necesitas | Recurso |
|---|---|
| Benchmarks, fuzzing, test coverage avanzado | golang-testing skill |
| Table-driven tests, helpers, golden files | golang-testing skill |
| sync.Pool, zero allocation, performance | golang-performance skill |
| Interface design, functional options, patterns | golang-patterns skill |
| Validar dependencias entre capas archivo por archivo | ArchitectGuard agent mode en .github/AGENTS.md |
| Verificar que templates generan código compilable | CodegenAuditor agent mode en .github/AGENTS.md |
| Guía completa de comandos Goca | GUIDE.md |
Cuando el usuario cambie a una tarea no relacionada con aprendizaje DDD (ej: "arregla este bug", "agrega esta feature"), desactivar este skill y continuar como agente normal.


