This commit is contained in:
2026-04-13 08:10:14 +03:00
commit a4455b1170
18 changed files with 1063 additions and 0 deletions
+32
View File
@@ -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/
+97
View File
@@ -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
View File
@@ -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)
}
}
+3
View File
@@ -0,0 +1,3 @@
module github.com/olezhek28/graceful-shutdown-demo
go 1.26.0
+105
View File
@@ -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)
}
}
+121
View File
@@ -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
}
+171
View File
@@ -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
}
+37
View File
@@ -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
}
+131
View File
@@ -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
}
+20
View File
@@ -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
}
+62
View File
@@ -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
}
+30
View File
@@ -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)
}
+24
View File
@@ -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 &notificationRepo{db: db}
}
+31
View File
@@ -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}
}
+24
View File
@@ -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}
}
+62
View File
@@ -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
}
+42
View File
@@ -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 &notificationService{
notificationRepo: notificationRepo,
userService: userService,
events: events,
}
}
+49
View File
@@ -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"
}