Write clean, maintainable Go code following Clean Architecture, dependency injection, and ChecklistApplication patterns. Use when writing new Go code, refactoring, or implementing features.
Scanned 2/12/2026
Install via CLI
openskills install majiayu000/claude-skill-registry---
name: go-maintainable-code
description: Write clean, maintainable Go code following Clean Architecture, dependency injection, and ChecklistApplication patterns. Use when writing new Go code, refactoring, or implementing features.
allowed-tools: Read, Write, Edit, Grep, Glob, Bash
---
# 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**:
```go
// 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**:
```go
// 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**:
```bash
./generate.sh # Regenerates wire_gen.go
```
## Code Quality Standards
### Error Handling
**Use domain.Error (custom error type)**:
```go
// ✅ 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**:
```go
// 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):
```go
// In controller
clientId := serverutils.GetClientIdFromContext(ctx)
```
### Transaction Handling
**Use connection.RunInTransaction**:
```go
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**:
```go
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](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**:
```yaml
# openapi/api_v1.yaml
paths:
/api/v1/checklists/{checklistId}/archive:
post:
operationId: archiveChecklist
# ... rest of spec
```
```go
// 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**:
```go
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**:
```go
// 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**:
```sql
FOREIGN KEY (parent_id) REFERENCES parent(id) ON DELETE CASCADE
```
**Named arguments** (pgx):
```go
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**:
```go
// 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
```go
// ❌ 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
```go
// ✅ 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](code-review-checklist.md) for complete review guide.
## Quick Reference
**Common commands**:
```bash
./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](go-patterns.md) - Language-specific best practices
- [Testing Guide](testing-guide.md) - Comprehensive testing examples
- [Code Review Checklist](code-review-checklist.md) - Quality checklist
- Project: [CLAUDE.md](../../../CLAUDE.md) - Full architecture guide
No comments yet. Be the first to comment!