Golang для веб-разработки: высокопроизводительные сервера и API

Пройдите тест, узнайте какой профессии подходите
Сколько вам лет
0%
До 18
От 18 до 24
От 25 до 34
От 35 до 44
От 45 до 49
От 50 до 54
Больше 55

Для кого эта статья:

  • Разработчики, интересующиеся веб-технологиями и языком программирования Go
  • Специалисты по бэкенд-разработке, ищущие эффективные инструменты для создания API
  • Студенты и начинающие программисты, желающие изучить современные подходы к разработке серверных приложений

    Язык Go — это не просто модный тренд в программировании, а мощный инструмент для создания высокопроизводительных веб-серверов и API. Разработанный инженерами Google, Golang обеспечивает беспрецедентную скорость выполнения кода, параллельную обработку запросов и низкое потребление ресурсов. Если вы устали от сложностей асинхронного программирования в Node.js или от перегруженности Java, самое время погрузиться в мир Go и на практике оценить, как его минималистичный синтаксис и встроенная поддержка конкурентности преобразят ваши веб-проекты. 🚀

Хотите не просто следовать инструкциям, а понять фундаментальные принципы веб-разработки? Курс Обучение веб-разработке от Skypro предлагает глубокое погружение в бэкенд-технологии, включая работу с высоконагруженными системами и микросервисной архитектурой. Изучая Golang в контексте реальных проектов под руководством практикующих разработчиков, вы получите не только теоретические знания, но и готовое портфолио для трудоустройства в ведущие IT-компании.

Преимущества Golang для создания веб-серверов и API

Golang уверенно занимает свою нишу в экосистеме языков программирования для бэкенд-разработки, предлагая уникальное сочетание производительности, простоты и надежности. Рассмотрим ключевые преимущества, которые делают Go оптимальным выбором для создания веб-серверов и REST API.

Алексей Петров, Tech Lead в финтех-стартапе

Наша команда столкнулась с классической проблемой — Node.js-сервер не справлялся с растущей нагрузкой, а время отклика API увеличилось до неприемлемых значений. Миграция на Go потребовала всего три недели, но результаты превзошли ожидания: время обработки запроса сократилось с 300 мс до 15 мс, а потребление памяти уменьшилось в 4 раза. При этом код стал проще поддерживать благодаря строгой типизации. Решающим фактором оказалась встроенная конкурентность Go — мы отказались от сложной асинхронной архитектуры в пользу простых горутин.

Golang создан с учетом современных требований к серверным приложениям, что делает его идеальным для разработки высоконагруженных веб-сервисов:

  • Высокая производительность — компилируемая природа языка и эффективный сборщик мусора обеспечивают скорость работы, сравнимую с C++ и Rust
  • Встроенная конкурентность — горутины и каналы предоставляют элегантный способ создания параллельных процессов с минимальными затратами ресурсов
  • Стандартная библиотека — богатый набор пакетов для работы с HTTP, JSON и криптографией позволяет создавать веб-серверы без привлечения сторонних зависимостей
  • Статическая типизация — значительно уменьшает количество ошибок на этапе компиляции, повышая надежность API
  • Кросс-компиляция — легко создавать бинарные файлы для различных ОС и архитектур без сложных настроек
Характеристика Golang Node.js Java Python
Время холодного старта Миллисекунды Секунды Десятки секунд Секунды
Параллелизм Горутины Однопоточный event loop Потоки GIL ограничения
Память на 1000 соединений ~5 МБ ~30 МБ ~100 МБ ~50 МБ
Компиляция Статическая Интерпретатор JVM Интерпретатор
Кривая обучения Средняя Низкая Высокая Низкая

Golang особенно выделяется в сценариях с высокой конкурентной нагрузкой, где требуется обрабатывать тысячи запросов одновременно. Стандартный пакет net/http способен масштабироваться до впечатляющих объемов трафика без необходимости в сложных настройках сервера.

Пошаговый план для смены профессии

Основы разработки HTTP-сервера в Golang с нуля

Создание базового HTTP-сервера на Go требует минимального количества кода благодаря мощным возможностям стандартной библиотеки. Даже без использования сторонних фреймворков, мы можем быстро реализовать функциональный веб-сервер. 💻

Для начала создадим простейший HTTP-сервер, который будет отвечать на GET-запросы:

go
Скопировать код
package main

import (
"fmt"
"net/http"
)

func main() {
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Привет, это мой первый golang веб-сервер и rest api!")
})

fmt.Println("Сервер запущен на http://localhost:8080")
http.ListenAndServe(":8080", nil)
}

Этот код использует пакет net/http для создания простого HTTP-сервера. Метод HandleFunc регистрирует функцию-обработчик для указанного маршрута, а ListenAndServe запускает сервер на порту 8080.

Теперь добавим обработку различных HTTP-методов и маршрутов:

go
Скопировать код
package main

import (
"encoding/json"
"fmt"
"log"
"net/http"
)

type Message struct {
Text string `json:"text"`
}

func main() {
// Обработчик для GET-запросов
http.HandleFunc("/api/hello", func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
w.WriteHeader(http.StatusMethodNotAllowed)
return
}

message := Message{Text: "Привет от golang веб-сервера и rest api!"}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(message)
})

// Обработчик для POST-запросов
http.HandleFunc("/api/echo", func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
w.WriteHeader(http.StatusMethodNotAllowed)
return
}

var message Message
if err := json.NewDecoder(r.Body).Decode(&message); err != nil {
w.WriteHeader(http.StatusBadRequest)
return
}

w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(message)
})

fmt.Println("Сервер запущен на http://localhost:8080")
log.Fatal(http.ListenAndServe(":8080", nil))
}

В этом примере мы создали два маршрута с разными HTTP-методами:

  • /api/hello (GET) — возвращает JSON с приветствием
  • /api/echo (POST) — принимает JSON в теле запроса и возвращает его обратно

Для организации более сложной маршрутизации в Go часто используют подход с созданием собственных типов обработчиков:

go
Скопировать код
type apiHandler struct {
// Здесь могут быть зависимости, например, доступ к БД
db *sql.DB
}

func (h *apiHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
path := r.URL.Path

switch {
case path == "/api/users" && r.Method == http.MethodGet:
h.getUsers(w, r)
case path == "/api/users" && r.Method == http.MethodPost:
h.createUser(w, r)
default:
w.WriteHeader(http.StatusNotFound)
}
}

func (h *apiHandler) getUsers(w http.ResponseWriter, r *http.Request) {
// Логика получения пользователей из БД
// ...
}

func (h *apiHandler) createUser(w http.ResponseWriter, r *http.Request) {
// Логика создания пользователя
// ...
}

// В функции main:
handler := &apiHandler{db: database}
http.Handle("/api/", handler)

Чтобы структурировать код веб-сервера, полезно следовать принципам чистой архитектуры, разделяя логику на слои:

  1. Транспортный слой (handlers) — обработка HTTP-запросов и ответов
  2. Сервисный слой (services) — бизнес-логика
  3. Репозиторий (repositories) — взаимодействие с базой данных
  4. Модели (models) — структуры данных

Такая организация кода делает его тестируемым и поддерживаемым, что особенно важно при создании сложных API.

Создание полноценного REST API на Golang

Полноценный REST API требует реализации всех основных HTTP-методов (GET, POST, PUT, DELETE), корректной обработки ошибок и валидации входных данных. Рассмотрим пример реализации CRUD-операций для управления ресурсами.

Для начала определим модель данных и хранилище:

go
Скопировать код
package main

import (
"encoding/json"
"fmt"
"log"
"net/http"
"strconv"
"strings"
"sync"
)

// Модель данных
type Task struct {
ID int `json:"id"`
Title string `json:"title"`
Description string `json:"description"`
Completed bool `json:"completed"`
}

// Хранилище данных (для простоты используем in-memory)
type TaskStore struct {
sync.RWMutex
tasks map[int]Task
nextTaskID int
}

// Создание нового хранилища
func NewTaskStore() *TaskStore {
return &TaskStore{
tasks: make(map[int]Task),
nextTaskID: 1,
}
}

// Метод для добавления задачи
func (s *TaskStore) Add(task Task) Task {
s.Lock()
defer s.Unlock()

task.ID = s.nextTaskID
s.nextTaskID++
s.tasks[task.ID] = task

return task
}

// Метод для получения всех задач
func (s *TaskStore) GetAll() []Task {
s.RLock()
defer s.RUnlock()

allTasks := make([]Task, 0, len(s.tasks))
for _, task := range s.tasks {
allTasks = append(allTasks, task)
}

return allTasks
}

// Метод для получения задачи по ID
func (s *TaskStore) Get(id int) (Task, bool) {
s.RLock()
defer s.RUnlock()

task, exists := s.tasks[id]
return task, exists
}

// Метод для обновления задачи
func (s *TaskStore) Update(id int, task Task) (Task, bool) {
s.Lock()
defer s.Unlock()

if _, exists := s.tasks[id]; !exists {
return Task{}, false
}

task.ID = id
s.tasks[id] = task

return task, true
}

// Метод для удаления задачи
func (s *TaskStore) Delete(id int) bool {
s.Lock()
defer s.Unlock()

if _, exists := s.tasks[id]; !exists {
return false
}

delete(s.tasks, id)
return true
}

Теперь создадим REST API для работы с этими задачами:

go
Скопировать код
// TaskHandler обрабатывает HTTP-запросы к API задач
type TaskHandler struct {
store *TaskStore
}

// Функция-обработчик всех запросов к API
func (h *TaskHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")

// Извлечение ID из пути
path := strings.Trim(r.URL.Path, "/")
pathParts := strings.Split(path, "/")

if len(pathParts) < 2 {
http.Error(w, "Некорректный путь", http.StatusBadRequest)
return
}

if pathParts[0] != "api" || pathParts[1] != "tasks" {
http.Error(w, "Неизвестный ресурс", http.StatusNotFound)
return
}

// Запрос к коллекции /api/tasks
if len(pathParts) == 2 {
switch r.Method {
case http.MethodGet:
h.getTasks(w, r)
case http.MethodPost:
h.createTask(w, r)
default:
http.Error(w, "Метод не поддерживается", http.StatusMethodNotAllowed)
}
return
}

// Запрос к элементу /api/tasks/{id}
if len(pathParts) == 3 {
id, err := strconv.Atoi(pathParts[2])
if err != nil {
http.Error(w, "Некорректный ID", http.StatusBadRequest)
return
}

switch r.Method {
case http.MethodGet:
h.getTask(w, r, id)
case http.MethodPut:
h.updateTask(w, r, id)
case http.MethodDelete:
h.deleteTask(w, r, id)
default:
http.Error(w, "Метод не поддерживается", http.StatusMethodNotAllowed)
}
return
}

http.Error(w, "Некорректный путь", http.StatusBadRequest)
}

// Получение списка всех задач
func (h *TaskHandler) getTasks(w http.ResponseWriter, r *http.Request) {
tasks := h.store.GetAll()
json.NewEncoder(w).Encode(tasks)
}

// Создание новой задачи
func (h *TaskHandler) createTask(w http.ResponseWriter, r *http.Request) {
var task Task
if err := json.NewDecoder(r.Body).Decode(&task); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}

// Валидация
if task.Title == "" {
http.Error(w, "Поле Title обязательно", http.StatusBadRequest)
return
}

createdTask := h.store.Add(task)
w.WriteHeader(http.StatusCreated)
json.NewEncoder(w).Encode(createdTask)
}

// Получение задачи по ID
func (h *TaskHandler) getTask(w http.ResponseWriter, r *http.Request, id int) {
task, exists := h.store.Get(id)
if !exists {
http.Error(w, "Задача не найдена", http.StatusNotFound)
return
}

json.NewEncoder(w).Encode(task)
}

// Обновление задачи
func (h *TaskHandler) updateTask(w http.ResponseWriter, r *http.Request, id int) {
var task Task
if err := json.NewDecoder(r.Body).Decode(&task); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}

// Валидация
if task.Title == "" {
http.Error(w, "Поле Title обязательно", http.StatusBadRequest)
return
}

updatedTask, exists := h.store.Update(id, task)
if !exists {
http.Error(w, "Задача не найдена", http.StatusNotFound)
return
}

json.NewEncoder(w).Encode(updatedTask)
}

// Удаление задачи
func (h *TaskHandler) deleteTask(w http.ResponseWriter, r *http.Request, id int) {
if deleted := h.store.Delete(id); !deleted {
http.Error(w, "Задача не найдена", http.StatusNotFound)
return
}

w.WriteHeader(http.StatusNoContent)
}

// Основная функция
func main() {
taskStore := NewTaskStore()
taskHandler := &TaskHandler{store: taskStore}

// Демонстрационные данные
taskStore.Add(Task{Title: "Изучить Golang", Description: "Освоить основы языка Go", Completed: false})
taskStore.Add(Task{Title: "Создать REST API", Description: "Разработать API на Go", Completed: false})

http.Handle("/api/tasks/", taskHandler)
http.Handle("/api/tasks", taskHandler)

fmt.Println("REST API сервер запущен на http://localhost:8080")
log.Fatal(http.ListenAndServe(":8080", nil))
}

Этот пример демонстрирует создание RESTful API с полным набором CRUD-операций. Для полноценного production-ready API стоит дополнить его:

  • Авторизацией и аутентификацией (JWT, OAuth)
  • Логированием запросов и ошибок
  • Ограничением скорости запросов (rate limiting)
  • Middleware для обработки CORS и других заголовков
  • Документацией API (Swagger/OpenAPI)

Популярные фреймворки для веб-разработки на Golang

Сергей Васильев, CTO стартапа

Когда наш проект превысил 50 000 строк кода, стандартная библиотека Go перестала справляться с организацией маршрутизации. Миграция на Echo заняла два дня, но стоила того. Особенно впечатлил встроенный валидатор и middleware для JWT. Производительность не пострадала: мы по-прежнему обрабатывали 5000 запросов в секунду на одном инстансе, но код стал чище и понятнее. Критическим фактором в пользу Echo стали встроенная привязка параметров и автоматическая валидация. При этом фреймворк не навязывал жесткую структуру проекта, что позволило сохранить привычную архитектуру.

Стандартная библиотека Go предоставляет отличную базу для веб-разработки, но для крупных проектов целесообразно использовать специализированные фреймворки. Они упрощают многие задачи и добавляют полезный функционал. 🛠️

Фреймворк Особенности Производительность Сложность изучения Популярность (GitHub ⭐)
Gin Минималистичный, быстрый, middleware-ориентированный Очень высокая Низкая 70k+
Echo Высокопроизводительный, расширяемый, минималистичный Высокая Низкая 27k+
Fiber Вдохновлен Express.js, быстрый и гибкий Очень высокая Средняя 28k+
Gorilla Модульный, расширяет стандартную библиотеку Высокая Низкая 18k+
Beego Полноценный фреймворк по типу Django/Rails Средняя Высокая 30k+

Gin Framework

Gin — один из самых популярных Go-фреймворков для создания веб-приложений и API. Его главные преимущества — скорость и минимализм.

go
Скопировать код
package main

import (
"github.com/gin-gonic/gin"
"net/http"
)

type Task struct {
ID int `json:"id"`
Title string `json:"title" binding:"required"`
Completed bool `json:"completed"`
}

var tasks = []Task{
{ID: 1, Title: "Изучить Gin Framework", Completed: false},
{ID: 2, Title: "Создать REST API", Completed: false},
}

func main() {
r := gin.Default() // Создает маршрутизатор с логгером и recovery middleware

// Группа маршрутов для API
api := r.Group("/api")
{
api.GET("/tasks", getTasks)
api.GET("/tasks/:id", getTaskByID)
api.POST("/tasks", createTask)
api.PUT("/tasks/:id", updateTask)
api.DELETE("/tasks/:id", deleteTask)
}

r.Run(":8080") // Запуск сервера на порту 8080
}

func getTasks(c *gin.Context) {
c.JSON(http.StatusOK, tasks)
}

func getTaskByID(c *gin.Context) {
id := c.Param("id")

for _, task := range tasks {
if string(task.ID) == id {
c.JSON(http.StatusOK, task)
return
}
}

c.JSON(http.StatusNotFound, gin.H{"message": "Task not found"})
}

func createTask(c *gin.Context) {
var newTask Task

// Валидация и привязка JSON к структуре
if err := c.ShouldBindJSON(&newTask); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}

// Установка ID для новой задачи
newTask.ID = len(tasks) + 1
tasks = append(tasks, newTask)

c.JSON(http.StatusCreated, newTask)
}

func updateTask(c *gin.Context) {
// Реализация обновления задачи
// ...
}

func deleteTask(c *gin.Context) {
// Реализация удаления задачи
// ...
}

Echo Framework

Echo — высокопроизводительный фреймворк с минималистичным API, но с более обширным функционалом:

go
Скопировать код
package main

import (
"github.com/labstack/echo/v4"
"github.com/labstack/echo/v4/middleware"
"net/http"
"strconv"
)

func main() {
e := echo.New()

// Middleware
e.Use(middleware.Logger())
e.Use(middleware.Recover())

// Маршруты
e.GET("/api/tasks", getTasks)
e.POST("/api/tasks", createTask)
e.GET("/api/tasks/:id", getTask)

e.Logger.Fatal(e.Start(":8080"))
}

func getTasks(c echo.Context) error {
return c.JSON(http.StatusOK, tasks)
}

func getTask(c echo.Context) error {
id, err := strconv.Atoi(c.Param("id"))
if err != nil {
return c.JSON(http.StatusBadRequest, map[string]string{
"error": "Invalid ID",
})
}

// Поиск задачи по ID
// ...

return c.JSON(http.StatusOK, task)
}

func createTask(c echo.Context) error {
var task Task

if err := c.Bind(&task); err != nil {
return c.JSON(http.StatusBadRequest, map[string]string{
"error": err.Error(),
})
}

// Валидация
if task.Title == "" {
return c.JSON(http.StatusBadRequest, map[string]string{
"error": "Title is required",
})
}

// Сохранение задачи
// ...

return c.JSON(http.StatusCreated, task)
}

При выборе фреймворка стоит учитывать следующие факторы:

  • Размер проекта — для небольших API достаточно стандартной библиотеки
  • Требуемая функциональность — некоторые фреймворки предлагают ORM, валидацию, шаблонизаторы
  • Производительность — Gin и Fiber оптимизированы для высоконагруженных систем
  • Опыт команды — если разработчики знакомы с определенным фреймворком, это может ускорить разработку
  • Долгосрочная поддержка — выбирайте активно поддерживаемые проекты с большим сообществом

Безопасность и оптимизация Golang веб-серверов

Разработка безопасного и оптимизированного веб-сервера на Golang требует внимания к множеству аспектов. Рассмотрим ключевые принципы и практики, которые помогут создать надежное API. 🔐

Безопасность веб-сервера

Безопасность REST API начинается с базовых принципов:

  • Аутентификация и авторизация — используйте JWT, OAuth или сессионные токены для проверки подлинности пользователей
  • Валидация входных данных — всегда проверяйте данные, поступающие от клиента
  • Защита от атак — внедряйте меры против CSRF, XSS, SQL-инъекций
  • HTTPS — используйте TLS для шифрования передаваемых данных
  • Управление зависимостями — регулярно обновляйте библиотеки для устранения уязвимостей

Пример реализации middleware для JWT-аутентификации:

go
Скопировать код
package main

import (
"fmt"
"net/http"
"strings"
"time"

"github.com/dgrijalva/jwt-go"
)

var jwtKey = []byte("your_secret_key")

// JWTMiddleware проверяет JWT токен в заголовке Authorization
func JWTMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Получение токена из заголовка Authorization
authHeader := r.Header.Get("Authorization")
if authHeader == "" {
http.Error(w, "Отсутствует авторизационный токен", http.StatusUnauthorized)
return
}

// Формат токена: "Bearer {token}"
tokenParts := strings.Split(authHeader, " ")
if len(tokenParts) != 2 || tokenParts[0] != "Bearer" {
http.Error(w, "Некорректный формат токена", http.StatusUnauthorized)
return
}

tokenString := tokenParts[1]

// Парсинг и валидация токена
token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
// Проверка метода подписи
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, fmt.Errorf("неожиданный метод подписи: %v", token.Header["alg"])
}

return jwtKey, nil
})

if err != nil {
http.Error(w, "Недействительный токен", http.StatusUnauthorized)
return
}

if claims, ok := token.Claims.(jwt.MapClaims); ok && token.Valid {
// Проверка срока действия токена
if exp, ok := claims["exp"].(float64); ok && int64(exp) < time.Now().Unix() {
http.Error(w, "Срок действия токена истек", http.StatusUnauthorized)
return
}

// Добавление информации о пользователе в контекст запроса
ctx := r.Context()
// Здесь можно добавить данные из токена в контекст

// Передача запроса следующему обработчику
next.ServeHTTP(w, r.WithContext(ctx))
return
}

http.Error(w, "Недействительный токен", http.StatusUnauthorized)
})
}

Для защиты от распространенных атак рекомендуется использовать специализированные middleware:

  • CORS middleware — для контроля доступа из браузера
  • Rate limiting — для защиты от DDoS-атак
  • Security headers — для защиты от XSS и других атак на стороне клиента
  • Request size limiting — для предотвращения атак типа "бомба JSON"

Оптимизация производительности

Golang изначально обеспечивает высокую производительность, но для максимальной эффективности следует учитывать:

  1. Кеширование — используйте in-memory кеширование или Redis для часто запрашиваемых данных
  2. Профилирование — регулярно анализируйте производительность с помощью pprof
  3. Оптимизация запросов к БД — используйте индексы, пулы соединений, оптимизируйте SQL-запросы
  4. Горизонтальное масштабирование — распределяйте нагрузку между несколькими инстансами
  5. Контроль памяти — минимизируйте аллокации и следите за утечками

Пример middleware для измерения времени обработки запросов:

go
Скопировать код
func MetricsMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()

// Оборачиваем ResponseWriter для доступа к статус-коду
wrapper := NewResponseWriterWrapper(w)

// Выполняем запрос
next.ServeHTTP(wrapper, r)

// Вычисляем время обработки
duration := time.Since(start)

// Логируем метрики
log.Printf(
"Method: %s, Path: %s, Status: %d, Duration: %s",
r.Method,
r.URL.Path,
wrapper.Status(),
duration,
)

// Здесь можно отправить метрики в систему мониторинга
// например, Prometheus
})
}

Для более глубокой оптимизации следует обратить внимание на:

  • JSON сериализацию — используйте более быстрые альтернативы стандартной библиотеке (easyjson, msgpack)
  • Пулы объектов — для часто используемых структур данных
  • Сжатие данных — gzip middleware для уменьшения объема передаваемых данных
  • Асинхронную обработку — для длительных операций используйте горутины и каналы

Для мониторинга производительности веб-сервера рекомендуется использовать:

  • Prometheus — для сбора метрик
  • Grafana — для визуализации
  • Jaeger или Zipkin — для трассировки запросов
  • pprof — для профилирования CPU и памяти

Golang для веб-разработки — это не просто технический выбор, а стратегическое решение. Невероятная производительность при минимальных ресурсах, встроенная конкурентность и компиляция в бинарный файл делают Go идеальным выбором для создания масштабируемых API и микросервисов. Что бы вы ни выбрали — стандартную библиотеку или специализированные фреймворки — Golang обеспечит стабильность, безопасность и скорость работы. Освоив инструментарий, представленный в этом руководстве, вы сможете создавать высоконагруженные веб-приложения, способные обрабатывать тысячи запросов в секунду с минимальным потреблением ресурсов.

Читайте также

Проверь как ты усвоил материалы статьи
Пройди тест и узнай насколько ты лучше других читателей
Какой язык программирования описывается в статье?
1 / 5

Загрузка...