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.
git clone https://github.com/majiayu000/claude-skill-registry
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"
skills/data/go-maintainable-code/SKILL.mdGo 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
frominternal/repositoryinternal/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
from service/repository methodsdomain.Error - Use
for new errorsdomain.NewError(message, statusCode) - Use
to wrap errorsdomain.Wrap(err, context, statusCode) - 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:
- Update
with new operationopenapi/api_v1.yaml - Run
to generate server interfaces./generate.sh - Implement generated interface in controller
- NEVER edit
files manually*_gen.go
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
if OpenAPI changed./generate.sh - 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
and all passgo test ./... - Ran
successfullygo build ./... - 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
- Go Patterns - Language-specific best practices
- Testing Guide - Comprehensive testing examples
- Code Review Checklist - Quality checklist
- Project: CLAUDE.md - Full architecture guide