Claude-skill-registry go-maintainable-code

Write clean, maintainable Go code following Clean Architecture, dependency injection, and ChecklistApplication patterns. Use when writing new Go code, refactoring, or implementing features.

install
source · Clone the upstream repo
git clone https://github.com/majiayu000/claude-skill-registry
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/majiayu000/claude-skill-registry "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/data/go-maintainable-code" ~/.claude/skills/majiayu000-claude-skill-registry-go-maintainable-code && rm -rf "$T"
manifest: skills/data/go-maintainable-code/SKILL.md
source content

Go Maintainable Code Skill

This skill ensures all Go code follows Clean Architecture principles and project-specific patterns used in ChecklistApplication.

Core Principles

1. Clean Architecture Layers (CRITICAL)

Dependency Flow:

server → service → repository

internal/
├── server/           # HTTP layer (Gin, OpenAPI controllers)
│   └── Depends on: service (via interfaces)
├── core/
│   ├── service/      # Business logic (framework-independent)
│   │   └── Depends on: repository interfaces, domain
│   ├── domain/       # Entities, value objects (no dependencies)
│   └── repository/   # Repository interfaces (no implementation)
└── repository/       # PostgreSQL implementations
    └── Depends on: repository interfaces, domain

Rules:

  • ✅ Server calls service interfaces
  • ✅ Service calls repository interfaces
  • ✅ Domain has NO external dependencies
  • ❌ NEVER import concrete types across layers
  • ❌ NEVER import
    internal/repository
    from
    internal/core/service

2. Interface-Based Design

Pattern from codebase:

// Define interface in core/repository
package repository
type IChecklistService interface {
    DeleteChecklistById(ctx context.Context, id uint) domain.Error
}

// Implement in core/service
package service
type checklistService struct {
    repository repository.IChecklistRepository  // Interface, not concrete
}

// Wire provides concrete implementation
// internal/deployment/wire.go

3. Dependency Injection via Wire

ALWAYS use Wire for dependencies:

// Add to internal/deployment/wire.go
func InitializeApp() (*App, error) {
    wire.Build(
        // ... existing providers ...
        NewMyService,           // Add your constructor
        wire.Bind(new(IMyService), new(*myService)),
    )
    return nil, nil
}

Then run:

./generate.sh  # Regenerates wire_gen.go

Code Quality Standards

Error Handling

Use domain.Error (custom error type):

// ✅ Good
func (s *service) Delete(ctx context.Context, id uint) domain.Error {
    if err := s.repo.Delete(ctx, id); err != nil {
        return domain.Wrap(err, "failed to delete", 500)
    }
    return nil
}

// ❌ Bad - using standard error
func (s *service) Delete(ctx context.Context, id uint) error {
    return errors.New("something failed")
}

Error patterns:

  • Return
    domain.Error
    from service/repository methods
  • Use
    domain.NewError(message, statusCode)
    for new errors
  • Use
    domain.Wrap(err, context, statusCode)
    to wrap errors
  • Guard rails return 404 for access denied (security pattern)

Context Usage

Extract user context:

// In service layer
userId, err := domain.GetUserIdFromContext(ctx)
if err != nil {
    return err
}

// Guard rail checks
if err := s.checklistOwnershipChecker.HasAccessToChecklist(ctx, checklistId); err != nil {
    return error.NewChecklistNotFoundError(checklistId)
}

Extract client ID (for SSE):

// In controller
clientId := serverutils.GetClientIdFromContext(ctx)

Transaction Handling

Use connection.RunInTransaction:

runQueryFunction := func(tx pool.TransactionWrapper) (ResultType, error) {
    // Execute queries using tx, not connection
    result, err := tx.Exec(ctx, query, args)
    return processedResult, err
}

res, err := connection.RunInTransaction(connection.TransactionProps[ResultType]{
    Query:      runQueryFunction,
    Connection: r.connection,
    TxOptions:  pgx.TxOptions{IsoLevel: pgx.Serializable},
})

Testing Requirements

Every service method needs tests:

func TestMyService_MethodName_SuccessCase(t *testing.T) {
    // Arrange
    mockRepo := new(mockRepository)
    mockRepo.On("Method", mock.Anything, expectedArgs).Return(expectedResult, nil)

    svc := &myService{repository: mockRepo}

    // Act
    result, err := svc.Method(context.Background(), args)

    // Assert
    if err != nil {
        t.Fatalf("unexpected error: %v", err)
    }
    mockRepo.AssertExpectations(t)
}

Test patterns:

  • Success case
  • Error cases
  • Guard rail failures
  • Edge cases (nil, empty, boundary values)

See testing-guide.md for complete examples.

Project-Specific Patterns

1. OpenAPI-First Development

Workflow:

  1. Update
    openapi/api_v1.yaml
    with new operation
  2. Run
    ./generate.sh
    to generate server interfaces
  3. Implement generated interface in controller
  4. NEVER edit
    *_gen.go
    files manually

Example:

# openapi/api_v1.yaml
paths:
  /api/v1/checklists/{checklistId}/archive:
    post:
      operationId: archiveChecklist
      # ... rest of spec
// internal/server/v1/checklist/controller.go
// Implements generated ServerInterface
func (c *controller) ArchiveChecklist(ctx context.Context, req ArchiveChecklistRequestObject) (ArchiveChecklistResponseObject, error) {
    // Implementation
}

2. SSE Notifications

After mutations, publish events:

func (s *service) Delete(ctx context.Context, id uint) domain.Error {
    if err := s.repository.Delete(ctx, id); err != nil {
        return err
    }

    // Publish SSE event
    s.notifier.NotifyItemDeleted(ctx, checklistId, id)
    return nil
}

SSE patterns:

  • Events filtered by Client ID (no echo to originating client)
  • Non-blocking publish with buffered channels
  • Guard rail check on subscribe

3. Database Patterns

Doubly-linked list ordering:

// Items use NEXT_ITEM_ID/PREV_ITEM_ID
// Use recursive CTE view: CHECKLIST_ITEMS_ORDERED_VIEW
// Phantom items: IS_PHANTOM = true, filtered in queries

CASCADE constraints:

FOREIGN KEY (parent_id) REFERENCES parent(id) ON DELETE CASCADE

Named arguments (pgx):

args := pgx.NamedArgs{
    "checklist_id": id,
    "user_id":      userId,
}
result, err := tx.Exec(ctx, "DELETE FROM t WHERE id = @checklist_id", args)

4. Struct Constructors

Private structs with public interfaces:

// Public interface
type IMyService interface {
    DoSomething(ctx context.Context) error
}

// Private implementation
type myService struct {
    repo repository.IMyRepository
}

// Public constructor for Wire
func NewMyService(repo repository.IMyRepository) IMyService {
    return &myService{repo: repo}
}

Anti-Patterns to Avoid

❌ Don't Do This

// ❌ Importing concrete types across layers
import "com.raunlo.checklist/internal/repository"

// ❌ Business logic in controllers
func (c *controller) Delete(ctx context.Context, req Request) Response {
    // Validating, processing here - NO!
}

// ❌ SQL in service layer
func (s *service) Find(ctx context.Context) {
    rows, _ := db.Query("SELECT ...") // NO!
}

// ❌ Not using guard rails
func (s *service) Delete(ctx context.Context, id uint) {
    return s.repo.Delete(ctx, id) // Missing access check!
}

// ❌ Hardcoded dependencies
type service struct {
    repo *postgresRepo  // Should be interface
}

// ❌ Ignoring errors
s.repo.Delete(ctx, id)  // No error handling

// ❌ Empty error messages
return domain.NewError("", 500)

✅ Do This Instead

// ✅ Interface imports only
import "com.raunlo.checklist/internal/core/repository"

// ✅ Thin controllers
func (c *controller) Delete(ctx context.Context, req Request) Response {
    domainCtx := serverutils.CreateContext(ctx)
    if err := c.service.DeleteById(domainCtx, req.Id); err != nil {
        return mapError(err)
    }
    return success()
}

// ✅ SQL in repository layer
func (r *repo) Find(ctx context.Context) ([]Entity, domain.Error) {
    rows, err := r.connection.Query(ctx, query)
    // ...
}

// ✅ Guard rail checks
func (s *service) Delete(ctx context.Context, id uint) domain.Error {
    if err := s.guardrail.HasAccessToChecklist(ctx, id); err != nil {
        return error.NewChecklistNotFoundError(id)
    }
    return s.repo.Delete(ctx, id)
}

// ✅ Interface dependencies
type service struct {
    repo repository.IMyRepository  // Interface
}

// ✅ Proper error handling
if err := s.repo.Delete(ctx, id); err != nil {
    return domain.Wrap(err, "failed to delete checklist", 500)
}

// ✅ Descriptive errors
return domain.NewError("Checklist is not empty", 400)

Checklist for New Code

Before submitting code, verify:

  • Follows Clean Architecture (correct layer separation)
  • Uses interfaces for dependencies
  • Added to Wire configuration if new service/repo
  • Ran
    ./generate.sh
    if OpenAPI changed
  • Proper error handling (domain.Error)
  • Guard rail checks for authorization
  • SSE notifications for mutations (if applicable)
  • Unit tests with mocks (testify)
  • No magic numbers or strings
  • Context passed through all layers
  • Ran
    go test ./...
    and all pass
  • Ran
    go build ./...
    successfully
  • No TODO comments without issue number

See code-review-checklist.md for complete review guide.

Quick Reference

Common commands:

./generate.sh           # OpenAPI + Wire code generation
go test ./...          # Run all tests
go build ./...         # Build all packages
go test ./internal/core/service -v -run TestMyTest  # Run specific test

File locations:

  • Controllers:
    internal/server/v1/
  • Services:
    internal/core/service/
  • Service interfaces:
    internal/core/repository/
  • Repository impls:
    internal/repository/
  • Domain entities:
    internal/core/domain/
  • SQL queries:
    internal/repository/query/
  • Wire config:
    internal/deployment/wire.go
  • OpenAPI spec:
    openapi/api_v1.yaml

Related Documentation