Agent skill
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 this agent skill to your Project
npx add-skill https://github.com/majiayu000/claude-skill-registry/tree/main/skills/development/go-maintainable-code-raunlo-checklistapplication
SKILL.md
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/repositoryfrominternal/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.Errorfrom 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:
- Update
openapi/api_v1.yamlwith new operation - Run
./generate.shto generate server interfaces - Implement generated interface in controller
- NEVER edit
*_gen.gofiles 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.shif 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
- Go Patterns - Language-specific best practices
- Testing Guide - Comprehensive testing examples
- Code Review Checklist - Quality checklist
- Project: CLAUDE.md - Full architecture guide
Didn't find tool you were looking for?