Сервис сокращения ссылок на Go с асинхронным сбором кликов и базовой статистикой.
Цель проекта: показать backend-навыки (REST API, PostgreSQL, Docker, конкурентность в Go через worker pool + channels, корректное завершение через context).
- Создание коротких ссылок через REST API
- Редирект по короткому коду
- Сбор кликов асинхронно (очередь на
channel+worker pool) - Статистика по ссылке:
total_clicks - Хранение данных в PostgreSQL
- Миграции БД через
goose - Конфигурация через переменные окружения
- Graceful shutdown (остановка воркеров через
context, дожим батча)
- Go
- HTTP: стандартный
net/http - PostgreSQL
- Docker + Docker Compose
- Миграции:
goose - Makefile для удобного управления проектом
Почему клики пишутся асинхронно?
Чтобы редирект был быстрым и не зависел от записи в БД на “горячем пути”.
Поток данных:
Client -> API
POST /links -> сохраняет ссылку в Postgres
GET /{code} -> читает original_url -> редиректит
-> отправляет click event в channel
Click workers (N goroutines) читают события из channel
-> батчат (batch size / flush interval)
-> вставляют пачкой в Postgres
GET /links/{code}/stats -> читает агрегаты из Postgres
Структура проекта
.
├── LICENSE
├── README.md
├── cmd
│ └── api
│ └── main.go
├── .env // создать свой в корне проекта
├── docker-compose.yml
├── go.mod
├── go.sum
├── internal
│ ├── config
│ │ └── config.go
│ ├── http
│ │ ├── handler
│ │ │ └── links.go
│ │ ├── httpx
│ │ │ ├── error.go
│ │ │ └── json.go
│ │ └── router.go
│ ├── repository
│ │ └── postgres
│ │ ├── db.go
│ │ └── links_repo.go
│ └── service
│ ├── errors.go
│ └── service.go
├── makefile
└── migrations
└── 20260109150617_init.sql
POST /links
curl -X POST http://localhost:8080/links \
-H "Content-Type: application/json" \
-d '{"url":"https://example.com"}'Пример ответа:
{
"code": "abc123",
"short_url": "http://localhost:8080/abc123"
}GET /{code}
curl -i http://localhost:8080/abc123Ожидаемо: 301/302 + Location: https://example.com
Параллельно создаётся событие клика (асинхронно).
GET /links/{code}/stats
curl http://localhost:8080/links/abc123/statsПример ответа:
{
"code": "abc123",
"total_clicks": 10
}- Docker и Docker Compose
- Go 1.21+ (для сборки)
- goose (установите:
go install github.com/pressly/goose/v3/cmd/goose@latest)
git clone <url-репозитория>
cd Shortener# Из примера
cp .env.example .envmake up
# или
docker compose up -dmake migratemake buildmake run
# или
./bin/shortener# Остановить PostgreSQL
make down| Команда | Описание |
|---|---|
make up |
Запустить PostgreSQL в Docker |
make down |
Остановить PostgreSQL |
make migrate |
Применить миграции БД |
make build |
Собрать бинарник в папку bin/ |
make run |
Запустить приложение из бинарника |
| Переменная | Пример / Default | Описание |
|---|---|---|
| APP_PORT | 8080 | Порт HTTP сервера |
| DATABASE_URL | postgres://postgres:postgres@localhost:5432/shortener?sslmode=disable | Строка подключения к Postgres |
| BASE_URL | http://localhost:8080 | Базовый адрес для сервера |
flowchart TB
Client[Client / Browser / API consumer]
subgraph App[Go service: URL Shortener API]
Router[HTTP Router]
Handler[Handlers]
Service["Service layer (business logic)"]
Repo["Repository (DB access)"]
ClickProducer["Click Producer puts events into channel"]
ClickChan[(chan ClickEvent)]
Workers[Worker Pool N goroutines]
Batcher["Batcher (size/time flush)"]
end
subgraph DB[PostgreSQL]
Links[(links table)]
Clicks[(clicks table)]
end
subgraph Infra[Infra]
Compose[docker-compose]
Migrations[goose migrations]
end
Client -->|POST /links| Router --> Service
Client -->|GET / code| Router --> Handler --> Service --> Repo
Handler -->|redirect 301/302| Client
Handler -->|async click event| ClickProducer --> ClickChan
ClickChan --> Workers --> Batcher --> Repo --> Clicks
Compose --- App
Compose --- DB
Migrations --> DB
- Swagger/OpenAPI документация
- Авторизация (API key / JWT)
- Добавить тесты
- Добавить логирование
- Создать сommon libraty (pkg)
# Остановите локальный PostgreSQL
sudo systemctl stop postgresql
# Или измените порт в docker-compose.ymlgo install github.com/pressly/goose/v3/cmd/goose@latestcp .env.example .env