Agent Skills › sazardev/goca

sazardev/goca

GitHub

生成极简且精确的 Conventional Commits 格式提交信息。遵循“为什么重于是什么”原则,严格限制主题长度并去除冗余噪音。仅在必要时添加正文解释原因或变更影响,不包含 AI 标识或文件重述,直接输出可粘贴的代码块。

5 skills 219

Install All Skills

npx skills add sazardev/goca --all -g -y
More Options

List skills in collection

npx skills add sazardev/goca --list

Skills in Collection (5)

生成极简且精确的 Conventional Commits 格式提交信息。遵循“为什么重于是什么”原则,严格限制主题长度并去除冗余噪音。仅在必要时添加正文解释原因或变更影响,不包含 AI 标识或文件重述,直接输出可粘贴的代码块。
用户指令:write a commit, commit message, generate commit, /commit, /caveman-commit 自动触发:暂存更改时
.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.

模拟原始人风格压缩文本,减少约75% Token消耗。支持lite至ultra及文言多种强度,自动保留技术准确性与代码块,适用于需极致精简回复的场景。
用户提及caveman mode或类似短语 请求节省Token或更简洁表达 调用/caveman命令
.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 users table 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.

提供Go语言惯用模式、最佳实践与约定,涵盖代码简洁性、零值设计、接口接受与结构体返回原则,以及错误处理(包装、自定义类型、Is/As检查),旨在构建健壮高效的Go应用。
编写新Go代码 审查Go代码 重构现有Go代码 设计Go包或模块
.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.

提供Go语言测试最佳实践,涵盖表格驱动测试、子测试、基准测试及模糊测试。遵循TDD方法论,指导用户编写可靠、可维护的测试代码,提升项目质量与开发效率。
编写新的Go函数或方法 为现有代码添加测试覆盖率 为性能关键代码创建基准测试 实施输入验证的模糊测试 在Go项目中遵循TDD工作流
.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.

基于 Goca CLI 的 DDD 与 Clean Architecture 教学工具。通过引导式六步流程,结合 TDD 实践,教授分层架构、依赖规则及核心概念(实体、用例等),帮助用户掌握高质量代码生成与架构设计。
用户询问如何学习 DDD 或 Clean Architecture 请求使用 Goca 生成特定组件如 entity、usecase 展示违反架构原则的代码寻求修正 要求逐步指导项目初始化与功能开发
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 DDDComando GocaCódigo generadoTest

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: handlerusecaserepositorydomain. 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 hacer errors.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:

  • ProductUseCase es interfaz pública. El handler programa contra esta interfaz, no contra el struct concreto.
  • productService es privado (minúscula). Solo se exporta el constructor NewProductService(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.
  • CreateProductInput vs domain.Product: el input puede tener validaciones distintas al domain. El Update usa punteros (*string) para distinguir "no enviado" de "enviado vacío".
  • La interfaz ProductRepository en usecase/interfaces.go es la misma que en repository/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.go define el contrato. El usecase la consume. El handler ni la conoce.
  • postgresProductRepository es privado — nadie fuera del package la instancia excepto el DI container.
  • El constructor NewPostgresProductRepository(db *gorm.DB) ProductRepository devuelve la interfaz, no el struct.
  • GORM está completamente encapsulado en este package. Si migras a MongoDB, cambias este archivo, no tocas domain ni usecase.
  • --transactions agrega SaveWithTx(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 usecase solo. No importa repository, no importa gorm. ✅
  • ProductHandler recibe usecase.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 usecase y domain no se tocan.
  • Routes se 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 que main.go necesita.

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.

Accueil - Wiki
Copyright © 2011-2026 iteam. Current version is 2.155.2. UTC+08:00, 2026-07-13 14:32
浙ICP备14020137号-1 $Carte des visiteurs$