import
This commit is contained in:
+32
@@ -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/
|
||||||
@@ -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 — база всегда закрывается последней, потому что создаётся первой.
|
||||||
+22
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
Vendored
+37
@@ -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
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
@@ -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}
|
||||||
|
}
|
||||||
@@ -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}
|
||||||
|
}
|
||||||
@@ -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}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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"
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user