算法艺术创作
algorithmic-art
通过代码和可调参数创作生成艺术与交互式视觉效果。
modular-design【设计与创意】Designs modular Go applications with clean architecture, dependency injection, package boundaries, and interface-based design for maintainable codebases.
独立 GitHub 快照 · 自动分类 · 以原作者文档为准
name: modular-design description: Designs modular Go applications with clean architecture, dependency injection, package boundaries, and interface-based design for maintainable codebases. license: MIT compatibility: opencode metadata: version: "1.0.0" domain: go role: implementation scope: implementation output-format: code content-types:
Senior architect designing clean, modular Go applications using dependency injection, interface boundaries, and clean architecture principles. This skill covers package organization, inversion of control, and scalable codebase structure.
domain/ or internal/ packages — independent of infrastructuremain) — never use new() in business logicUse this skill when:
Avoid this skill for:
Define the Domain Model — Identify core entities, value objects, and domain behaviors.
Checkpoint: Domain types have no external dependencies — no imports from infra, api, or config.
Define Interface Boundaries — Identify what the domain needs from external services (repositories, gateways). Checkpoint: Interfaces are defined in the domain package, implemented in the infrastructure package.
Set Up Dependency Injection — Create a composition root where all dependencies are wired together.
Checkpoint: main() constructs the full dependency graph and passes it to the application entry point.
Organize Packages by Layer — Group code by architectural layer, not by technical type. Checkpoint: Each package depends only on inner layers — never outward.
Verify No Circular Dependencies — Run go list -deps and review import cycles.
Checkpoint: go mod graph shows no circular import chains.
Go packages should be organized by domain layer, not by technical type.
myapp/
├── handlers/ # All HTTP handlers mixed together
├── services/ # All business logic mixed together
├── models/ # All data structures mixed together
├── repositories/ # All DB access mixed together
├── utils/ # All helper functions mixed together
└── main.go
What's wrong:
utils/ becomes a dumping ground for unrelated helper functionsmyapp/
├── cmd/
│ └── server/
│ └── main.go # Composition root: wires all dependencies
├── internal/
│ ├── user/ # User domain module
│ │ ├── domain/
│ │ │ ├── user.go # User entity/value object
│ │ │ └── errors.go # Domain-specific errors
│ │ ├── repository.go # Interface: UserRepository (defined here)
│ │ ├── service.go # Service: uses repository, contains business logic
│ │ └── usecase/
│ │ └── create_user.go # Specific use case implementation
│ │
│ ├── order/ # Order domain module
│ │ ├── domain/
│ │ ├── repository.go
│ │ └── service.go
│ │
│ └── infrastructure/
│ ├── db/
│ │ └── postgres/
│ │ └── user_repository.go # Implements user.Repository
│ └── api/
│ └── http/
│ ├── handlers.go # HTTP handlers (thin layer)
│ └── middleware.go
│
├── go.mod
└── go.sum
Why this works:
user/, order/) is self-containeddomain/ — no external dependenciesrepository.go) — consumed where neededinfrastructure/ — depends on domain interfacesDependency injection at the composition root enables testability and flexibility.
// ❌ BAD: service creates its dependencies directly — impossible to mock
package service
import "myapp/internal/infrastructure/db/postgres"
type UserService struct {
// Direct concrete type — cannot substitute a mock or alternate implementation
db *postgres.Client
}
func NewUserService() *UserService {
// Creates a real database connection — no way to inject a test DB
client := postgres.NewClient("postgres://localhost/myapp")
return &UserService{db: client}
}
// CreateUser depends on a real database — cannot be tested in isolation
func (s *UserService) CreateUser(ctx context.Context, name string) (*User, error) {
user := &User{Name: name, ID: uuid.New()}
if err := s.db.InsertUser(ctx, user); err != nil {
return nil, fmt.Errorf("create user: %w", err)
}
return user, nil
}
What's wrong:
UserService creates its own database connection — no testabilitypostgres.Client for unit testsNewUserService() has a side effect (opens a DB connection)infrastructure/db/postgres — domain depends on infrastructure (violates dependency rule)// package: internal/user
//
// Repository interface defined in the domain package.
// Infrastructure implements it, domain consumes it.
type Repository interface {
Create(ctx context.Context, user *User) error
GetByID(ctx context.Context, id string) (*User, error)
Update(ctx context.Context, user *User) error
Delete(ctx context.Context, id string) error
List(ctx context.Context, page, limit int) ([]*User, error)
}
// Service contains business logic for the user domain.
// All dependencies are injected — no direct instantiation of infrastructure.
type Service struct {
repo Repository
logger Logger
}
// NewService creates a user service with the given dependencies.
func NewService(repo Repository, logger Logger) *Service {
return &Service{
repo: repo,
logger: logger,
}
}
// CreateUser validates and creates a new user via the repository.
func (s *Service) CreateUser(ctx context.Context, name string) (*User, error) {
if name == "" {
return nil, fmt.Errorf("create user: name must not be empty")
}
user := &User{
Name: strings.TrimSpace(name),
ID: uuid.New().String(),
State: UserStateActive,
}
if err := s.repo.Create(ctx, user); err != nil {
return nil, fmt.Errorf("create user: %w", err)
}
s.logger.Info("user created", "id", user.ID)
return user, nil
}
Composition root — cmd/server/main.go:
// main constructs the full dependency graph and starts the server.
func main() {
ctx := context.Background()
// Infrastructure: create real dependencies
db := postgres.NewClient(ctx, dbConfig)
logger := zerolog.New(os.Stdout).With().Timestamp().Logger()
cache := redis.NewClient(redisConfig)
// Domain: inject infrastructure into services
userRepo := postgres.NewUserRepository(db)
userService := user.NewService(userRepo, logger)
orderService := order.NewService(orderRepo, userRepo, logger)
// API: inject services into handlers
handler := api.NewHandler(userService, orderService, logger)
// Server: inject handlers
server := cloud.NewServer(cfg, handler)
if err := server.Run(ctx); err != nil {
log.Fatal(err)
}
}
Why this works:
UserService depends on Repository interface — not postgres.ClientNewService() has no side effects — no DB connection openedRepository to NewService()main.go) is the single place where infrastructure is createdDomain-specific error types enable precise error handling at the composition root.
// ❌ BAD: string-based errors — hard to pattern match, impossible to wrap meaningfully
func (s *Service) CreateUser(ctx context.Context, name string) (*User, error) {
if name == "" {
return nil, errors.New("name cannot be empty") // flat error — no context
}
if len(name) > 100 {
return nil, errors.New("name too long") // another flat error
}
user := &User{Name: name}
if err := s.db.Insert(ctx, user); err != nil {
// Generic DB error — caller can't distinguish unique constraint from connection error
return nil, fmt.Errorf("failed to create user: %w", err)
}
return user, nil
}
What's wrong:
strings.Contains to match errors — brittle and slow%w loses the domain context (validation vs. DB)// package: internal/user/domain
// AppError is a domain-specific error with a code and message.
type AppError struct {
Code string
Message string
Details map[string]string
}
func (e *AppError) Error() string {
return e.Message
}
func (e *AppError) Is(target error) bool {
_, ok := target.(*AppError)
return ok
}
// Sentinel errors for common domain conditions.
var (
ErrNotFound = &AppError{Code: "NOT_FOUND", Message: "resource not found"}
ErrValidationError = &AppError{Code: "VALIDATION_ERROR", Message: "invalid input"}
ErrConflict = &AppError{Code: "CONFLICT", Message: "resource already exists"}
)
// NewValidationError creates a validation error with field details.
func NewValidationError(field, message string) *AppError {
return &AppError{
Code: "VALIDATION_ERROR",
Message: message,
Details: map[string]string{field: message},
}
}
// IsAppError checks if an error is an AppError (or wraps one).
func IsAppError(err error) bool {
var appErr *AppError
return errors.As(err, &appErr)
}
// package: internal/user
// CreateUser validates input and creates the user.
// Returns domain-specific errors that the caller can handle appropriately.
func (s *Service) CreateUser(ctx context.Context, name string) (*User, error) {
// Validation errors use domain types
if name == "" {
return nil, NewValidationError("name", "name must not be empty")
}
if len(name) > 100 {
return nil, NewValidationError("name", "name must be 100 characters or less")
}
user := &User{Name: strings.TrimSpace(name), ID: uuid.New().String()}
if err := s.repo.Create(ctx, user); err != nil {
// Check for unique constraint violation — map to domain error
if isDuplicateKeyError(err) {
return nil, ErrConflict
}
return nil, fmt.Errorf("create user: %w", err)
}
return user, nil
}
// package: cmd/server
// handleCreateUser is an HTTP handler that converts domain errors to HTTP responses.
func (h *Handler) handleCreateUser(w http.ResponseWriter, r *http.Request) {
// ... parse request ...
user, err := h.userService.CreateUser(r.Context(), name)
if err != nil {
// Handle domain errors with appropriate HTTP status codes
if appErr, ok := err.(*user.AppError); ok {
switch appErr.Code {
case "VALIDATION_ERROR":
http.Error(w, appErr.Message, http.StatusBadRequest)
return
case "CONFLICT":
http.Error(w, appErr.Message, http.StatusConflict)
return
case "NOT_FOUND":
http.Error(w, appErr.Message, http.StatusNotFound)
return
}
}
// Non-domain errors are internal server errors
log.Printf("unexpected error: %v", err)
http.Error(w, "internal server error", http.StatusInternalServerError)
return
}
// Success — write response
writeJSON(w, http.StatusCreated, user)
}
Why this works:
IsAppError() enables programmatic error handlingerrors.As() works with domain errors — no string matching needednew() or &Type{} in business logicmain.go) as the single place where infrastructure types are createderrors.As to check themvar db *Client at package level)new() in business logic — always accept dependencies as parameters| Skill | Purpose |
|---|---|
best-practices | Go idioms, naming, and error handling conventions |
cloud-development | Cloud-native patterns for modular services (shutdown, config, health) |
database-patterns | Repository patterns and data access layer design |
advanced-patterns | Functional options and generics for flexible dependency injection |