commit a4455b1170c11bfc537901b941203598ebdb8b3c Author: liver Date: Mon Apr 13 08:10:14 2026 +0300 import diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ed20272 --- /dev/null +++ b/.gitignore @@ -0,0 +1,32 @@ +# If you prefer the allow list template instead of the deny list, see community template: +# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore +# +# Binaries for programs and plugins +*.exe +*.exe~ +*.dll +*.so +*.dylib + +# Test binary, built with `go test -c` +*.test + +# Code coverage profiles and other test artifacts +*.out +coverage.* +*.coverprofile +profile.cov + +# Dependency directories (remove the comment below to include it) +# vendor/ + +# Go workspace file +go.work +go.work.sum + +# env file +.env + +# Editor/IDE +.idea/ +.vscode/ diff --git a/README.md b/README.md new file mode 100644 index 0000000..e0b9e22 --- /dev/null +++ b/README.md @@ -0,0 +1,97 @@ +## 👋 Привет! Я Олег Козырев + +Staff Golang Engineer · Ex Ozon, Avito, Tinkoff + +📺 [YouTube](https://www.youtube.com/@olezhek28go) · 💬 [Telegram](https://t.me/olezhek28go) + +--- + +# Graceful Shutdown Demo + +Учебный Go-проект, демонстрирующий паттерн **graceful shutdown** — корректное завершение приложения с дожиданием текущих запросов и освобождением ресурсов. + +## Что внутри + +Полноценное HTTP-приложение с несколькими слоями: + +- **HTTP API** — три эндпоинта: `/health`, `/users/me`, `/slow` +- **Сервисы** — бизнес-логика (UserService, AuthService, NotificationService) +- **Репозитории** — слой данных (UserRepo, SessionRepo, NotificationRepo) +- **Инфраструктура** — база данных (PostgreSQL), кэш (Redis), шина событий (EventBus) +- **DI-контейнер** — ленивая инициализация зависимостей с автоматической регистрацией в closer +- **Closer** — менеджер graceful shutdown (LIFO-порядок закрытия ресурсов) + +## Как работает graceful shutdown + +1. `signal.NotifyContext` перехватывает **SIGINT** (Ctrl+C) и **SIGTERM** (Kubernetes) +2. `server.Shutdown()` — перестаёт принимать новые соединения и дожидает текущие запросы (таймаут 15с) +3. `closer.CloseAll()` — закрывает все ресурсы в обратном порядке: кэш, затем БД (таймаут 10с) +4. Суммарный бюджет: 15с + 10с = 25с из 30с Kubernetes grace period + +**Паттерн «двойной Ctrl+C»:** первый — graceful shutdown, второй — мгновенное завершение (для разработки, если shutdown завис). + +## Как попробовать + +```bash +# Запуск +go run cmd/main.go + +# В другом терминале — медленный запрос +curl localhost:8080/slow + +# Пока запрос висит — нажмите Ctrl+C в терминале сервера +# Без graceful shutdown: curl получит "connection reset by peer" +# С graceful shutdown: curl дождётся ответа "готово!" — 200 OK +``` + +## Структура проекта + +``` +cmd/ + main.go # Точка входа +internal/ + app/ + app.go # Жизненный цикл приложения, graceful shutdown + di.go # DI-контейнер с ленивой инициализацией + api/ + server.go # HTTP-хендлеры и маршрутизация + closer/ + closer.go # Менеджер закрытия ресурсов (LIFO) + config/ + config.go # Конфигурация (DSN, Redis, HTTP-порт) + database/ + database.go # Абстракция базы данных + cache/ + cache.go # Абстракция кэша (Redis) + events/ + bus.go # Шина событий (pub/sub) + service/ + user.go # Бизнес-логика пользователей + auth.go # Авторизация + notification.go # Уведомления + repository/ + user.go # Доступ к данным пользователей + session.go # Доступ к данным сессий + notification.go # Доступ к данным уведомлений +``` + +## Граф зависимостей + +``` +Handler +├── UserService +│ ├── UserRepo → DB +│ ├── AuthService +│ │ ├── UserRepo → DB +│ │ ├── SessionRepo → DB + Cache +│ │ ├── Cache +│ │ └── EventBus +│ └── EventBus +├── AuthService +└── NotificationService + ├── NotificationRepo → DB + ├── UserService + └── EventBus +``` + +Порядок закрытия (LIFO): EventBus → Cache → DB — база всегда закрывается последней, потому что создаётся первой. \ No newline at end of file diff --git a/cmd/main.go b/cmd/main.go new file mode 100644 index 0000000..6f5da50 --- /dev/null +++ b/cmd/main.go @@ -0,0 +1,22 @@ +package main + +import ( + "log/slog" + "os" + + "github.com/olezhek28/graceful-shutdown-demo/internal/app" +) + +func main() { + // slog работает из коробки — никакой инициализации не нужно. + // Дефолтный логгер пишет в stderr в текстовом формате. + // Для продакшена можно настроить JSON: + // slog.SetDefault(slog.New(slog.NewJSONHandler(os.Stdout, nil))) + + a := app.New() + + if err := a.Run(); err != nil { + slog.Error("ошибка приложения", "err", err) + os.Exit(1) + } +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..ef2eb34 --- /dev/null +++ b/go.mod @@ -0,0 +1,3 @@ +module github.com/olezhek28/graceful-shutdown-demo + +go 1.26.0 diff --git a/internal/api/server.go b/internal/api/server.go new file mode 100644 index 0000000..4eb9c0f --- /dev/null +++ b/internal/api/server.go @@ -0,0 +1,105 @@ +package api + +import ( + "fmt" + "log/slog" + "net/http" + "time" +) + +// UserService — интерфейс сервиса пользователей для хендлера. +type UserService interface { + GetProfile(token string) string +} + +// AuthService — интерфейс сервиса авторизации для хендлера. +type AuthService interface { + // методы, которые хендлер использует из сервиса авторизации +} + +// NotificationService — интерфейс сервиса уведомлений для хендлера. +type NotificationService interface { + // методы, которые хендлер использует из сервиса уведомлений +} + +// Handler — интерфейс обработчика HTTP-запросов. +type Handler interface { + Routes() http.Handler +} + +// handler — конкретная реализация, скрыта от внешних пакетов. +// Содержит только хендлеры и роутинг. +// Ничего не знает про http.Server, порт или lifecycle — +// это ответственность app-слоя. +type handler struct { + userService UserService + authService AuthService + notificationService NotificationService +} + +// NewHandler создаёт обработчик HTTP-запросов. +func NewHandler( + userService UserService, + authService AuthService, + notificationService NotificationService, +) Handler { + return &handler{ + userService: userService, + authService: authService, + notificationService: notificationService, + } +} + +// Routes возвращает маршрутизатор со всеми зарегистрированными хендлерами. +// App-слой использует этот http.Handler при создании http.Server. +func (h *handler) Routes() http.Handler { + mux := http.NewServeMux() + mux.HandleFunc("GET /health", h.healthHandler) + mux.HandleFunc("GET /users/me", h.getUserProfile) + + // /slow — эндпоинт для демонстрации graceful shutdown. + // Имитирует долгий запрос: обращение к БД, вызов внешнего API и т.д. + // Пять секунд — чтобы мы успели нажать Ctrl+C, пока запрос выполняется. + mux.HandleFunc("GET /slow", h.slowHandler) + + return mux +} + +func (h *handler) healthHandler(w http.ResponseWriter, _ *http.Request) { + w.WriteHeader(http.StatusOK) + + if _, err := fmt.Fprintln(w, "ok"); err != nil { + slog.Error("ошибка записи ответа", "err", err) + } +} + +func (h *handler) getUserProfile(w http.ResponseWriter, r *http.Request) { + token := r.Header.Get("Authorization") + profile := h.userService.GetProfile(token) + + w.WriteHeader(http.StatusOK) + + if _, err := fmt.Fprintln(w, profile); err != nil { + slog.Error("ошибка записи ответа", "err", err) + } +} + +// slowHandler — медленный эндпоинт для демонстрации graceful shutdown. +// Имитирует реальную работу: запрос в базу, вызов внешнего сервиса. +// +// Попробуйте: +// 1. curl localhost:8080/slow +// 2. Пока запрос висит — нажмите Ctrl+C в терминале сервера +// 3. Без graceful shutdown: curl получит "connection reset by peer" +// 4. С graceful shutdown: curl дождётся и получит "готово!" — 200 OK +func (h *handler) slowHandler(w http.ResponseWriter, _ *http.Request) { + slog.Info("обрабатываю медленный запрос...") + + time.Sleep(5 * time.Second) // имитация: запрос в БД, внешний API + + w.WriteHeader(http.StatusOK) + + if _, err := fmt.Fprintln(w, "готово!"); err != nil { + slog.Error("ошибка записи ответа", "err", err) + } +} diff --git a/internal/app/app.go b/internal/app/app.go new file mode 100644 index 0000000..7d46441 --- /dev/null +++ b/internal/app/app.go @@ -0,0 +1,121 @@ +package app + +import ( + "context" + "errors" + "log/slog" + "net/http" + "os/signal" + "syscall" + "time" + + "github.com/olezhek28/graceful-shutdown-demo/internal/closer" + "github.com/olezhek28/graceful-shutdown-demo/internal/config" +) + +// App — структура приложения. +// Содержит DI-контейнер и HTTP-сервер. +type App struct { + diContainer *diContainer + httpServer *http.Server +} + +// New создаёт приложение и инициализирует все зависимости через DI-контейнер. +func New() *App { + a := &App{ + diContainer: newDIContainer(), + } + + a.initDeps() + + return a +} + +// initDeps последовательно вызывает функции инициализации. +func (a *App) initDeps() { + inits := []func(){ + a.initHTTPServer, + } + + for _, fn := range inits { + fn() + } +} + +// initHTTPServer создаёт HTTP-сервер. +func (a *App) initHTTPServer() { + a.httpServer = &http.Server{ + Addr: config.AppConfig().HTTPAddr, + Handler: a.diContainer.Handler().Routes(), + } +} + +// Run запускает HTTP-сервер с graceful shutdown. +// +// Что происходит: +// 1. signal.NotifyContext перехватывает SIGINT (Ctrl+C) и SIGTERM (Kubernetes) +// 2. HTTP-сервер запускается в отдельной горутине +// 3. Main-горутина ждёт сигнал через <-ctx.Done() +// 4. При сигнале: server.Shutdown дожидается текущих запросов +// 5. closer.CloseAll закрывает все ресурсы в обратном порядке (LIFO) +// +// Паттерн "двойной Ctrl+C": +// - Первый Ctrl+C → graceful shutdown +// - stop() снимает custom handler после первого сигнала +// - Второй Ctrl+C → ОС убивает процесс мгновенно (для разработки, когда shutdown завис) +func (a *App) Run() error { + // 1. Перехват сигналов. + // signal.NotifyContext создаёт канал с ёмкостью 1 (буферизованный). + // Если бы канал был unbuffered — сигнал мог бы потеряться, + // пока main ещё инициализирует зависимости и не слушает канал. + ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM) + defer stop() + + slog.Info("сервер запущен", "addr", config.AppConfig().HTTPAddr) + + // 2. Запуск сервера в горутине. + // ListenAndServe блокирует — поэтому запускаем в горутине, а main ждёт сигнал. + // http.ErrServerClosed — нормальное завершение (мы сами вызвали Shutdown), не ошибка. + go func() { + if err := a.httpServer.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) { + slog.Error("ошибка сервера", "err", err) + } + }() + + // 3. Ожидание сигнала. + <-ctx.Done() + slog.Info("получен сигнал, завершаем...") + + // Паттерн "двойной Ctrl+C": снимаем custom handler. + // Теперь второй Ctrl+C убьёт процесс мгновенно (дефолтное поведение ОС). + stop() + + // 4. Graceful shutdown HTTP-сервера. + // Таймаут 15 секунд. Используем context.Background(), а не ctx — тот уже отменён. + // + // Что делает server.Shutdown внутри: + // 1) Закрывает listeners — новые TCP-соединения невозможны + // 2) Закрывает idle connections (keep-alive без активных запросов) + // 3) Ждёт активные connections — пока handler вернёт ответ + // 4) Если контекст истёк — возвращает ошибку, НО handlers продолжают работать в фоне + shutdownCtx, shutdownCancel := context.WithTimeout(context.Background(), 15*time.Second) + defer shutdownCancel() + + if err := a.httpServer.Shutdown(shutdownCtx); err != nil { + slog.Error("ошибка при остановке сервера", "err", err) + } + + slog.Info("сервер остановлен") + + // 5. Закрытие всех ресурсов через глобальный closer (LIFO). + // Отдельный контекст с таймаутом 10 секунд — свой бюджет для ресурсов. + // Суммарно: 15с (сервер) + 10с (ресурсы) = 25с из 30с Kubernetes grace period. + closerCtx, closerCancel := context.WithTimeout(context.Background(), 10*time.Second) + defer closerCancel() + + if err := closer.CloseAll(closerCtx); err != nil { + slog.Error("ошибки при закрытии ресурсов", "err", err) + } + + return nil +} diff --git a/internal/app/di.go b/internal/app/di.go new file mode 100644 index 0000000..ee91b20 --- /dev/null +++ b/internal/app/di.go @@ -0,0 +1,171 @@ +package app + +import ( + "context" + "log/slog" + "os" + + "github.com/olezhek28/graceful-shutdown-demo/internal/api" + "github.com/olezhek28/graceful-shutdown-demo/internal/cache" + "github.com/olezhek28/graceful-shutdown-demo/internal/closer" + "github.com/olezhek28/graceful-shutdown-demo/internal/config" + "github.com/olezhek28/graceful-shutdown-demo/internal/database" + "github.com/olezhek28/graceful-shutdown-demo/internal/events" + "github.com/olezhek28/graceful-shutdown-demo/internal/repository" + "github.com/olezhek28/graceful-shutdown-demo/internal/service" +) + +// diContainer — контейнер зависимостей с ленивой инициализацией. +// +// Это тот же контейнер из видео про DI, но при создании каждого ресурса +// с методом Close() мы сразу регистрируем его в глобальном closer. +// closer.Add() вызывается прямо в геттере — одна строка, и ресурс +// автоматически закроется при graceful shutdown в правильном LIFO-порядке. +// +// Почему порядок закрытия детерминирован при ленивой инициализации: +// initDeps() вызывается в одной горутине. Ленивая инициализация — это +// depth-first обход графа зависимостей. Порядок определяется графом: +// +// Handler() → UserService() → UserRepo() → DB() ← 1-й closer.Add +// → AuthService() → SessionRepo() → Cache() ← 2-й closer.Add +// +// DB всегда создаётся раньше Cache (UserRepo запрашивает DB до того, +// как SessionRepo запросит Cache). Граф не меняется → порядок не меняется. +type diContainer struct { + // Инфраструктура + db database.DB + cache cache.Cache + eventBus events.EventBus + + // Репозитории + userRepo repository.UserRepo + sessionRepo repository.SessionRepo + notificationRepo repository.NotificationRepo + + // Сервисы + authService service.AuthService + userService service.UserService + notificationService service.NotificationService + + // API + handler api.Handler +} + +// newDIContainer создаёт новый пустой контейнер. +func newDIContainer() *diContainer { + return &diContainer{} +} + +// DB возвращает подключение к базе данных. +// При создании — сразу регистрирует Close() в глобальном closer. +// БД создаётся одной из первых — значит закроется одной из последних (LIFO). +func (d *diContainer) DB() database.DB { + if d.db == nil { + db, err := database.New(config.AppConfig().DSN) + if err != nil { + slog.Error("не удалось подключиться к БД", "err", err) + os.Exit(1) + } + + closer.Add("база данных", func(_ context.Context) error { + return db.Close() + }) + + d.db = db + } + + return d.db +} + +// EventBus возвращает шину событий. +func (d *diContainer) EventBus() events.EventBus { + if d.eventBus == nil { + d.eventBus = events.NewEventBus() + } + + return d.eventBus +} + +// Cache возвращает подключение к кэшу. +// При создании — сразу регистрирует Close() в глобальном closer. +// Кэш создаётся после БД — значит закроется раньше БД (LIFO). +func (d *diContainer) Cache() cache.Cache { + if d.cache == nil { + c := cache.New(config.AppConfig().RedisAddr) + + closer.Add("кэш", func(_ context.Context) error { + return c.Close() + }) + + d.cache = c + } + + return d.cache +} + +// UserRepo возвращает репозиторий пользователей. +func (d *diContainer) UserRepo() repository.UserRepo { + if d.userRepo == nil { + d.userRepo = repository.NewUserRepo(d.DB()) + } + + return d.userRepo +} + +// SessionRepo возвращает репозиторий сессий. +func (d *diContainer) SessionRepo() repository.SessionRepo { + if d.sessionRepo == nil { + d.sessionRepo = repository.NewSessionRepo(d.DB(), d.Cache()) + } + + return d.sessionRepo +} + +// NotificationRepo возвращает репозиторий уведомлений. +func (d *diContainer) NotificationRepo() repository.NotificationRepo { + if d.notificationRepo == nil { + d.notificationRepo = repository.NewNotificationRepo(d.DB()) + } + + return d.notificationRepo +} + +// AuthService возвращает сервис авторизации. +func (d *diContainer) AuthService() service.AuthService { + if d.authService == nil { + d.authService = service.NewAuthService(d.UserRepo(), d.SessionRepo(), d.Cache(), d.EventBus()) + } + + return d.authService +} + +// UserService возвращает сервис пользователей. +func (d *diContainer) UserService() service.UserService { + if d.userService == nil { + d.userService = service.NewUserService(d.UserRepo(), d.AuthService(), d.EventBus()) + } + + return d.userService +} + +// NotificationService возвращает сервис уведомлений. +func (d *diContainer) NotificationService() service.NotificationService { + if d.notificationService == nil { + d.notificationService = service.NewNotificationService(d.NotificationRepo(), d.UserService(), d.EventBus()) + } + + return d.notificationService +} + +// Handler возвращает HTTP-хендлер. +func (d *diContainer) Handler() api.Handler { + if d.handler == nil { + d.handler = api.NewHandler( + d.UserService(), + d.AuthService(), + d.NotificationService(), + ) + } + + return d.handler +} diff --git a/internal/cache/cache.go b/internal/cache/cache.go new file mode 100644 index 0000000..7f16c61 --- /dev/null +++ b/internal/cache/cache.go @@ -0,0 +1,37 @@ +package cache + +import "log/slog" + +// Cache — интерфейс подключения к кэшу. +type Cache interface { + Get(key string) (string, error) + Set(key, value string) error + Close() error +} + +// redisCache — конкретная реализация, скрыта от внешних пакетов. +type redisCache struct { + addr string +} + +// New создаёт подключение к кэшу. +func New(addr string) Cache { + slog.Info("подключились к кэшу", "addr", addr) + return &redisCache{addr: addr} +} + +// Get получает значение по ключу. +func (c *redisCache) Get(key string) (string, error) { + return "", nil +} + +// Set устанавливает значение по ключу. +func (c *redisCache) Set(key, value string) error { + return nil +} + +// Close закрывает подключение к кэшу. +func (c *redisCache) Close() error { + slog.Info("подключение к кэшу закрыто") + return nil +} diff --git a/internal/closer/closer.go b/internal/closer/closer.go new file mode 100644 index 0000000..142be62 --- /dev/null +++ b/internal/closer/closer.go @@ -0,0 +1,131 @@ +package closer + +import ( + "context" + "errors" + "log/slog" + "sync" + "time" +) + +// closeFn — одна функция закрытия с именем ресурса. +// Имя нужно для логирования: при shutdown видно, какой именно ресурс закрывается. +type closeFn struct { + name string + fn func(context.Context) error +} + +// closer управляет graceful shutdown приложения. +// +// Принцип работы — как defer: последний добавленный ресурс закрывается первым (LIFO). +// Это важно для зависимостей: если кэш добавлен после базы данных, +// то при shutdown кэш закроется первым, а база — последней. +// Так гарантируется, что ни один ресурс не обращается к уже закрытой зависимости. +// +// Потокобезопасен: Add() можно вызывать из разных горутин +// при ленивой инициализации зависимостей в DI-контейнере. +// CloseAll() безопасен для повторного вызова — выполнится только один раз (sync.Once). +// +// Структура приватная — снаружи пакета доступны только функции Add() и CloseAll(), +// работающие с глобальным экземпляром. +type closer struct { + mu sync.Mutex // защищает слайс funcs от конкурентной записи + once sync.Once // гарантирует что CloseAll выполнится только один раз + funcs []closeFn // накопленные функции закрытия в порядке добавления +} + +// globalCloser — глобальный экземпляр. +// Позволяет вызывать closer.Add() и closer.CloseAll() из любого места, +// не передавая экземпляр через конструкторы и DI-контейнер. +var globalCloser = &closer{} + +// Add добавляет функцию закрытия в глобальный closer. +// Вызывается при создании каждого ресурса, например: +// +// closer.Add("база данных", func(_ context.Context) error { +// return db.Close() +// }) +func Add(name string, fn func(context.Context) error) { + globalCloser.add(name, fn) +} + +// CloseAll вызывает все функции закрытия глобального closer-а в обратном порядке (LIFO). +// Принимает context с таймаутом — если ресурс не закрылся вовремя, context отменится. +// +// Важно: таймаут в ctx — общий на все ресурсы, а не на каждый по отдельности. +// Если БД закрывалась 20 секунд из 25 — на кэш останется только 5. +// +// Безопасен для повторного вызова — выполнится только один раз. +func CloseAll(ctx context.Context) error { + return globalCloser.closeAll(ctx) +} + +// add добавляет функцию закрытия с именем ресурса. +func (c *closer) add(name string, fn func(context.Context) error) { + c.mu.Lock() + defer c.mu.Unlock() + + c.funcs = append(c.funcs, closeFn{name: name, fn: fn}) +} + +// closeAll вызывает все зарегистрированные функции закрытия в обратном порядке (LIFO). +// +// Порядок закрытия — обратный порядку добавления: +// +// closer.Add("база данных", dbClose) // добавлен первым +// closer.Add("кэш", cacheClose) // добавлен вторым +// closer.Add("HTTP-сервер", srvStop) // добавлен третьим +// +// При вызове CloseAll: +// 1. HTTP-сервер (добавлен последним — закрывается первым) +// 2. кэш +// 3. база данных (добавлена первой — закрывается последней) +// +// Если один ресурс не закрылся — остальные всё равно закроются. +func (c *closer) closeAll(ctx context.Context) error { + var result error + + c.once.Do(func() { + // Забираем все функции под мьютексом и обнуляем слайс, + // чтобы не держать ссылки на ресурсы после закрытия. + c.mu.Lock() + funcs := c.funcs + c.funcs = nil + c.mu.Unlock() + + if len(funcs) == 0 { + return + } + + slog.Info("начинаем плавное завершение", "count", len(funcs)) + + var errs []error + + // Идём от конца к началу — LIFO, как defer. + for i := len(funcs) - 1; i >= 0; i-- { + f := funcs[i] + + start := time.Now() + slog.Info("закрываем ресурс", "name", f.name) + + if err := f.fn(ctx); err != nil { + // Логируем ошибку, но продолжаем закрывать остальные ресурсы. + slog.Error("ошибка при закрытии ресурса", + "name", f.name, + "error", err, + "duration", time.Since(start), + ) + + errs = append(errs, err) + } else { + slog.Info("ресурс закрыт", "name", f.name, "duration", time.Since(start)) + } + } + + slog.Info("все ресурсы закрыты") + + result = errors.Join(errs...) + }) + + return result +} diff --git a/internal/config/config.go b/internal/config/config.go new file mode 100644 index 0000000..adde49b --- /dev/null +++ b/internal/config/config.go @@ -0,0 +1,20 @@ +package config + +// Config хранит конфигурацию приложения. +type Config struct { + DSN string + RedisAddr string + HTTPAddr string +} + +// appConfig — глобальный экземпляр конфигурации. +var appConfig = &Config{ + DSN: "postgres://localhost:5432/demo", + RedisAddr: "localhost:6379", + HTTPAddr: ":8080", +} + +// AppConfig возвращает конфигурацию приложения. +func AppConfig() *Config { + return appConfig +} diff --git a/internal/database/database.go b/internal/database/database.go new file mode 100644 index 0000000..937e286 --- /dev/null +++ b/internal/database/database.go @@ -0,0 +1,62 @@ +package database + +import ( + "fmt" + "log/slog" +) + +// DB — интерфейс подключения к базе данных. +type DB interface { + Query(query string) error + QueryRow(query string) error + Exec(query string) error + BeginTx() error + BulkInsert(query string, args ...any) error + Close() error +} + +// db — конкретная реализация, скрыта от внешних пакетов. +type db struct { + dsn string +} + +// New создаёт подключение к базе данных. +func New(dsn string) (DB, error) { + if dsn == "" { + return nil, fmt.Errorf("dsn пустой") + } + + slog.Info("подключились к базе данных", "dsn", dsn) + return &db{dsn: dsn}, nil +} + +// Query выполняет запрос на чтение. +func (d *db) Query(query string) error { + return nil +} + +// QueryRow выполняет запрос, возвращающий одну строку. +func (d *db) QueryRow(query string) error { + return nil +} + +// Exec выполняет запрос на запись. +func (d *db) Exec(query string) error { + return nil +} + +// BeginTx начинает транзакцию. +func (d *db) BeginTx() error { + return nil +} + +// BulkInsert выполняет массовую вставку. +func (d *db) BulkInsert(query string, args ...any) error { + return nil +} + +// Close закрывает подключение к базе данных. +func (d *db) Close() error { + slog.Info("подключение к базе данных закрыто") + return nil +} diff --git a/internal/events/bus.go b/internal/events/bus.go new file mode 100644 index 0000000..f54c6c4 --- /dev/null +++ b/internal/events/bus.go @@ -0,0 +1,30 @@ +package events + +import "log/slog" + +// EventBus — интерфейс шины событий приложения. +// Чистый pub/sub брокер без внешних зависимостей. +// Сервисы сами публикуют события и подписываются на них. +type EventBus interface { + Publish(event string) + Subscribe(event string, handler func()) +} + +// eventBus — конкретная реализация, скрыта от внешних пакетов. +type eventBus struct{} + +// NewEventBus создаёт шину событий. +func NewEventBus() EventBus { + slog.Info("шина событий создана") + return &eventBus{} +} + +// Publish публикует событие. +func (b *eventBus) Publish(event string) { + slog.Info("событие опубликовано", "event", event) +} + +// Subscribe подписывается на событие. +func (b *eventBus) Subscribe(event string, handler func()) { + slog.Info("подписка на событие", "event", event) +} diff --git a/internal/repository/notification.go b/internal/repository/notification.go new file mode 100644 index 0000000..1c3c1af --- /dev/null +++ b/internal/repository/notification.go @@ -0,0 +1,24 @@ +package repository + +import "log/slog" + +// NotificationDB — интерфейс базы данных, который нужен NotificationRepo. +type NotificationDB interface { + Query(query string) error + Exec(query string) error + BulkInsert(query string, args ...any) error +} + +// NotificationRepo — интерфейс репозитория уведомлений. +type NotificationRepo interface{} + +// notificationRepo — конкретная реализация, скрыта от внешних пакетов. +type notificationRepo struct { + db NotificationDB +} + +// NewNotificationRepo создаёт репозиторий уведомлений. +func NewNotificationRepo(db NotificationDB) NotificationRepo { + slog.Info("репозиторий уведомлений создан") + return ¬ificationRepo{db: db} +} diff --git a/internal/repository/session.go b/internal/repository/session.go new file mode 100644 index 0000000..8f75bf3 --- /dev/null +++ b/internal/repository/session.go @@ -0,0 +1,31 @@ +package repository + +import "log/slog" + +// SessionDB — интерфейс базы данных, который нужен SessionRepo. +type SessionDB interface { + Query(query string) error + Exec(query string) error + BeginTx() error +} + +// SessionCache — интерфейс кэша, который нужен SessionRepo. +type SessionCache interface { + Get(key string) (string, error) + Set(key, value string) error +} + +// SessionRepo — интерфейс репозитория сессий. +type SessionRepo interface{} + +// sessionRepo — конкретная реализация, скрыта от внешних пакетов. +type sessionRepo struct { + db SessionDB + cache SessionCache +} + +// NewSessionRepo создаёт репозиторий сессий. +func NewSessionRepo(db SessionDB, cache SessionCache) SessionRepo { + slog.Info("репозиторий сессий создан") + return &sessionRepo{db: db, cache: cache} +} diff --git a/internal/repository/user.go b/internal/repository/user.go new file mode 100644 index 0000000..0102fea --- /dev/null +++ b/internal/repository/user.go @@ -0,0 +1,24 @@ +package repository + +import "log/slog" + +// UserDB — интерфейс базы данных, который нужен UserRepo. +type UserDB interface { + Query(query string) error + QueryRow(query string) error + Exec(query string) error +} + +// UserRepo — интерфейс репозитория пользователей. +type UserRepo interface{} + +// userRepo — конкретная реализация, скрыта от внешних пакетов. +type userRepo struct { + db UserDB +} + +// NewUserRepo создаёт репозиторий пользователей. +func NewUserRepo(db UserDB) UserRepo { + slog.Info("репозиторий пользователей создан") + return &userRepo{db: db} +} diff --git a/internal/service/auth.go b/internal/service/auth.go new file mode 100644 index 0000000..547ddeb --- /dev/null +++ b/internal/service/auth.go @@ -0,0 +1,62 @@ +package service + +import "log/slog" + +// AuthUserRepository — интерфейс репозитория пользователей для AuthService. +type AuthUserRepository interface { + // методы, которые AuthService использует из репозитория пользователей +} + +// AuthSessionRepository — интерфейс репозитория сессий для AuthService. +type AuthSessionRepository interface { + // методы, которые AuthService использует из репозитория сессий +} + +// AuthCache — интерфейс кэша для AuthService (хранит токены). +type AuthCache interface { + Get(key string) (string, error) + Set(key, value string) error +} + +// AuthEventPublisher — интерфейс для публикации событий авторизации. +type AuthEventPublisher interface { + Publish(event string) +} + +// AuthService — интерфейс сервиса авторизации. +type AuthService interface { + ValidateToken(token string) bool +} + +// authService — конкретная реализация, скрыта от внешних пакетов. +type authService struct { + userRepo AuthUserRepository + sessionRepo AuthSessionRepository + cache AuthCache + events AuthEventPublisher +} + +// NewAuthService создаёт сервис авторизации. +func NewAuthService( + userRepo AuthUserRepository, + sessionRepo AuthSessionRepository, + cache AuthCache, + events AuthEventPublisher, +) AuthService { + slog.Info("сервис авторизации создан") + return &authService{ + userRepo: userRepo, + sessionRepo: sessionRepo, + cache: cache, + events: events, + } +} + +// ValidateToken проверяет токен авторизации. +func (s *authService) ValidateToken(token string) bool { + slog.Info("проверяем токен", "token", token) + + _, err := s.cache.Get(token) + + return err == nil +} diff --git a/internal/service/notification.go b/internal/service/notification.go new file mode 100644 index 0000000..4a19e49 --- /dev/null +++ b/internal/service/notification.go @@ -0,0 +1,42 @@ +package service + +import "log/slog" + +// NotificationRepository — интерфейс репозитория уведомлений для NotificationService. +type NotificationRepository interface { + // методы, которые NotificationService использует из репозитория уведомлений +} + +// NotificationUserService — интерфейс сервиса пользователей для NotificationService. +type NotificationUserService interface { + // методы, которые NotificationService использует из сервиса пользователей +} + +// NotificationEventSubscriber — интерфейс для подписки на события. +type NotificationEventSubscriber interface { + Subscribe(event string, handler func()) +} + +// NotificationService — интерфейс сервиса уведомлений. +type NotificationService interface{} + +// notificationService — конкретная реализация, скрыта от внешних пакетов. +type notificationService struct { + notificationRepo NotificationRepository + userService NotificationUserService + events NotificationEventSubscriber +} + +// NewNotificationService создаёт сервис уведомлений. +func NewNotificationService( + notificationRepo NotificationRepository, + userService NotificationUserService, + events NotificationEventSubscriber, +) NotificationService { + slog.Info("сервис уведомлений создан") + return ¬ificationService{ + notificationRepo: notificationRepo, + userService: userService, + events: events, + } +} diff --git a/internal/service/user.go b/internal/service/user.go new file mode 100644 index 0000000..0924e65 --- /dev/null +++ b/internal/service/user.go @@ -0,0 +1,49 @@ +package service + +import "log/slog" + +// UserRepository — интерфейс репозитория пользователей для UserService. +type UserRepository interface { + // методы, которые UserService использует из репозитория пользователей +} + +// UserAuthService — интерфейс сервиса авторизации для UserService. +type UserAuthService interface { + ValidateToken(token string) bool +} + +// UserEventPublisher — интерфейс для публикации событий пользователей. +type UserEventPublisher interface { + Publish(event string) +} + +// UserService — интерфейс сервиса пользователей. +type UserService interface { + GetProfile(token string) string +} + +// userService — конкретная реализация, скрыта от внешних пакетов. +type userService struct { + userRepo UserRepository + authService UserAuthService + events UserEventPublisher +} + +// NewUserService создаёт сервис пользователей. +func NewUserService(userRepo UserRepository, authService UserAuthService, events UserEventPublisher) UserService { + slog.Info("сервис пользователей создан") + return &userService{ + userRepo: userRepo, + authService: authService, + events: events, + } +} + +// GetProfile возвращает профиль текущего пользователя. +func (s *userService) GetProfile(token string) string { + if !s.authService.ValidateToken(token) { + return "unauthorized" + } + + return "user profile data" +}