Представьте: вы создаёте API, которое должно пережить тенденции фреймворков, выдержать миграции баз данных и пройти через неизбежные встречи под девизом «давайте перепишем всё на Rust». Добро пожаловать в мир чистой архитектуры в Go, где мы разделяем проблемы, словно дипломаты, распределяющие спорные территории. Сегодня мы создадим организованный API TODO, который будет более упорядоченным, чем полочка для специй у моей бабушки.

Закладываем фундамент

Начнём с создания основы нашего проекта:

go mod init github.com/yourname/todo-clean

Теперь давайте установим наш набор инструментов для выживания в цифровом мире:

go get github.com/gorilla/mux github.com/jmoiron/sqlx \
github.com/lib/pq github.com/kelseyhightower/envconfig

Наша структура каталогов будет выглядеть следующим образом:

todo-clean/
├── cmd/
│   └── api/
│       └── main.go
├── internal/
│   ├── entity/
│   ├── usecase/
│   ├── delivery/
│   └── repository/

Эта структура делает наш код таким же организованным, как квартира чемпиона по тетрису.

Раскрываем чистые слои

graph TD A[HTTP-запрос] --> B[Слой доставки] B --> C[Уровень прецедентов использования] C --> D[Слой репозитория] D --> E[PostgreSQL] E --> D D --> C C --> B B --> A

1. Уровень сущностей — наша бизнес-ДНК

В entity/todo.go:

package entity
type Todo struct {
    ID          string `json:"id" db:"id"`
    Title       string `json:"title" validate:"required"`
    Description string `json:"description"`
    Completed   bool   `json:"completed"`
}
type TodoRepository interface {
    Store(todo *Todo) error
    FindByID(id string) (*Todo, error)
    Delete(id string) error
}

Эти сущности — конституция нашего приложения, они не заботятся о HTTP или базах данных, только о бизнес-правилах.

2. Уровень прецедентов использования — мозг

В usecase/todo.go:

package usecase
type TodoInteractor struct {
    repo entity.TodoRepository
}
func (ti *TodoInteractor) CreateTodo(t entity.Todo) error {
    if t.Title == "" {
        return errors.New("title cannot be empty")
    }
    return ti.repo.Store(&t)
}
// Добавьте другие методы CRUD

Здесь живёт бизнес-логика, полностью независимая от фреймворка. Это как учить свою собаку командам — собаке всё равно, говорите ли вы по-английски или на клингонском.

3. Слой доставки — посланник

В delivery/http/handler.go:

package http
func NewTodoHandler(usecase usecase.TodoInteractor) *mux.Router {
    r := mux.NewRouter()
    r.HandleFunc("/todos", createTodoHandler(usecase)).Methods("POST")
    // Добавьте другие маршруты
    return r
}
func createTodoHandler(uc usecase.TodoInteractor) http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) {
        var todo entity.Todo
        json.NewDecoder(r.Body).Decode(&todo)
        if err := uc.CreateTodo(todo); err != nil {
            w.WriteHeader(http.StatusBadRequest)
            json.NewEncoder(w).Encode(map[string]string{"error": err.Error()})
            return
        }
        w.WriteHeader(http.StatusCreated)
    }
}

Наши HTTP-обработчики — это просто переводчики, преобразующие HTTP-команды в команды бизнес-логики.

4. Слой репозиториев — оборотень

В repository/postgres/todo.go:

package postgres
type TodoRepo struct {
    db *sqlx.DB
}
func (tr *TodoRepo) Store(t *entity.Todo) error {
    _, err := tr.db.NamedExec(`
        INSERT INTO todos (id, title, description, completed)
        VALUES (:id, :title, :description, :completed)`, t)
    return err
}
// Реализуйте другие методы репозитория

Смена баз данных похожа на замену шин — автомобиль (бизнес-логика) продолжает движение.

Соединяем всё вместе

В cmd/api/main.go:

func main() {
    var cfg config.AppConfig
    envconfig.MustProcess("TODO", &cfg)
    db := sqlx.MustConnect("postgres", cfg.DatabaseURL)
    defer db.Close()
    todoRepo := repository.NewPostgresTodoRepo(db)
    todoUseCase := usecase.NewTodoInteractor(todoRepo)
    handler := delivery.NewTodoHandler(todoUseCase)
    log.Printf("Запуск сервера на %s", cfg.ServerAddress)
    log.Fatal(http.ListenAndServe(cfg.ServerAddress, handler))
}

Эта основная функция — мастер марионеток, координирующий все наши уровни.

Тестирование: наша страховочная сеть

func TestTodoCreation(t *testing.T) {
    mockRepo := new(MockTodoRepo)
    useCase := usecase.NewTodoInteractor(mockRepo)
    t.Run("valid todo", func(t *testing.T) {
        err := useCase.CreateTodo(entity.Todo{Title: "Test"})
        assert.Nil(t, err)
    })
    t.Run("empty title", func(t *testing.T) {
        err := useCase.CreateTodo(entity.Todo{Title: ""})
        assert.ErrorContains(t, err, "не может быть пустым")
    })
}

Наши тесты доказывают, что бизнес-правила сохраняются, даже если завтра мы перейдём на MongoDB.

Результат: гибкость на первом месте

Нужно добавить gRPC? Просто создайте новую реализацию доставки. Хотите кэшировать задачи? Создайте декоратор репозитория. Это как LEGO для backend-разработчиков. Вот и всё — чистая архитектура в Go, которая более удобна в обслуживании, чем Tesla. Помните, хорошая архитектура заключается не в следовании тенденциям, а в создании систем, которые переживут неверные решения вашего будущего «я». А теперь вперёд и организуйте эти кодовые базы, как будто вы упорядочиваете стартап в духе Мари Кондо!