Итак, вы хотите создать платформу для онлайн-семинаров. Возможно, вы заметили, насколько сильно мир нуждается в лучших способах обучения людей программированию на Go — языку, который удивительно прагматичен, но преступно недооценён в сообществе разработчиков. Или, возможно, вы просто устали от сбоев вебинаров в Zoom, когда одновременно подключаются 500 разработчиков (привет, проблемы с пропускной способностью). В любом случае вы выбрали Go, и это именно то, что нужно.

Почему Go? Потому что Go — это швейцарский армейский нож в backend-разработке: он поддерживает параллелизм по умолчанию, компилируется в единый бинарный файл и заставляет вашу команду DevOps улыбаться, как будто они только что получили бесплатный кофе. Создание платформы для семинаров на Go — это не просто технический выбор; это философский выбор. Вы привержены простоте, производительности и надёжности, которая позволяет вам спать по ночам.

Эта статья поможет вам создать готовую к использованию платформу для онлайн-семинаров, которая поддерживает взаимодействие в реальном времени, постоянное хранение данных и масштабируемость. Мы пропустим общие фразы и сразу перейдём к архитектуре, реализации и тому коду, который вы фактически будете использовать.

Понимание архитектуры

Прежде чем начать писать код, давайте обсудим, что мы на самом деле создаём. Платформа для семинаров должна обрабатывать:

  • Обмен сообщениями в реальном времени между инструкторами и участниками.
  • Управление сеансами (начало, пауза, окончание семинаров).
  • Отслеживание участников и разрешения на основе ролей.
  • Обмен ресурсами (слайды, фрагменты кода, упражнения).
  • Трансляцию событий (когда кто-то задаёт вопрос, все должны об этом знать).

Вот как эти компоненты сочетаются друг с другом:

graph TB Client1["Веб-клиент 1"] Client2["Веб-клиент 2"] Client3["Веб-клиент 3"] LB["Балансор нагрузки"] API["API шлюз"] WS1["WebSocket сервер 1"] WS2["WebSocket сервер 2"] Cache["Redis кэш"] DB["PostgreSQL"] EventBus["Шина событий"] Client1 -->|HTTP/WebSocket| LB Client2 -->|HTTP/WebSocket| LB Client3 -->|HTTP/WebSocket| LB LB --> API API --> WS1 API --> WS2 WS1 --> Cache WS2 --> Cache WS1 --> DB WS2 --> DB WS1 --> EventBus WS2 --> EventBus EventBus --> WS1 EventBus --> WS2

Эта архитектура намеренно проста. Мы используем встроенную модель параллелизма Go для обработки нескольких WebSocket-соединений без каких-либо проблем, Redis для кэширования состояния сеансов для молниеносного поиска и PostgreSQL для надёжного хранения данных. Шина событий гарантирует, что когда что-то происходит на одном сервере, все подключённые клиенты на всех серверах сразу об этом узнают.

Основные компоненты: фундамент

Давайте создадим базовые типы и структуры, от которых будет зависеть всё остальное. Думайте об этом как о нашем контракте с реальностью — эти типы определяют, с чем именно мы имеем дело.

package models
import (
    "time"
)
// Workshop представляет собой один сеанс онлайн-семинара
type Workshop struct {
    ID          string    `json:"id" db:"id"`
    Title       string    `json:"title" db:"title"`
    Description string    `json:"description" db:"description"`
    InstructorID string   `json:"instructor_id" db:"instructor_id"`
    Status      string    `json:"status" db:"status"` // "запланировано", "в прямом эфире", "завершено"
    StartTime   time.Time `json:"start_time" db:"start_time"`
    EndTime     time.Time `json:"end_time" db:"end_time"`
    MaxParticipants int   `json:"max_participants" db:"max_participants"`
    CreatedAt   time.Time `json:"created_at" db:"created_at"`
}
// Participant представляет человека, посещающего семинар
type Participant struct {
    ID         string    `json:"id" db:"id"`
    WorkshopID string    `json:"workshop_id" db:"workshop_id"`
    UserID     string    `json:"user_id" db:"user_id"`
    Role       string    `json:"role" db:"role"` // "инструктор", "модератор", "участник"
    JoinedAt   time.Time `json:"joined_at" db:"joined_at"`
    LeftAt     *time.Time `json:"left_at" db:"left_at"`
}
// Message представляет сообщение в чате или событие на семинаре
type Message struct {
    ID         string    `json:"id" db:"id"`
    WorkshopID string    `json:"workshop_id" db:"workshop_id"`
    SenderID   string    `json:"sender_id" db:"sender_id"`
    Type       string    `json:"type" db:"type"` // "чат", "вопрос", "объявление"
    Content    string    `json:"content" db:"content"`
    CreatedAt  time.Time `json:"created_at" db:"created_at"`
}
// Event представляет события в реальном времени, транслируемые всем участникам
type Event struct {
    Type      string    `json:"type"`      // "пользователь_присоединился", "сообщение", "слайд_изменился"
    WorkshopID string   `json:"workshop_id"`
    Payload   map[string]interface{} `json:"payload"`
    Timestamp time.Time `json:"timestamp"`
}

Почему такая структура? Потому что Go ценит явность. Мы ничего не скрываем в динамических картах или кошмарах с преобразованием JSON. Теги базы данных сообщают нашему ORM, куда именно сохранять эти данные, а теги JSON обрабатывают сериализацию. Чисто, предсказуемо, отлаживаемо.

Создание WebSocket хаба

Сердцем нашей платформы является WebSocket хаб — именно здесь происходит всё волшебство. Когда пользователь подключается, он присоединяется к хабу, специфичному для его семинара. Когда происходит любое событие, хаб транслирует его всем подключённым клиентам.

package hub
import (
    "fmt"
    "sync"
    "time"
)
// Client представляет подключённого пользователя WebSocket
type Client struct {
    ID         string
    WorkshopID string
    UserID     string
    Role       string
    conn       chan interface{}
    quit       chan bool
}
// Hub управляет всеми клиентами для конкретного семинара
type Hub struct {
    workshopID string
    clients    map[string]*Client
    broadcast  chan interface{}
    register   chan *Client
    unregister chan *Client
    mu         sync.RWMutex
}
// NewHub создаёт новый хаб для семинара
func NewHub(workshopID string) *Hub {
    return &Hub{
        workshopID: workshopID,
        clients:    make(map[string]*Client),
        broadcast:  make(chan interface{}, 100),
        register:   make(chan *Client),
        unregister: make(chan *Client),
    }
}
// Run запускает цикл событий хаба — эта goroutine никогда не завершается
func (h *Hub) Run() {
    for {
        select {
        case client := <-h.register:
            h.mu.Lock()
            h.clients[client.ID] = client
            h.mu.Unlock()
            fmt.Printf("Клиент %s присоединился к семинару %s\n", client.ID, h.workshopID)
        case client := <-h.unregister:
            h.mu.Lock()
            if _, ok := h.clients[client.ID]; ok {
                delete(h.clients, client.ID)
                close(client.conn)
            }
            h.mu.Unlock()
            fmt.Printf("Клиент %s покинул семинар %s\n", client.ID, h.workshopID)
        case message := <-h.broadcast:
            h.mu.RLock()
            for _, client := range h.clients {
                select {
                case client.conn <- message:
                case <-time.After(100 * time.Millisecond):
                    // Если канал клиента заполнен, пропускаем это сообщение
                    // Это предотвращает блокировку хаба медленными клиентами
                }
            }
            h.mu.RUnlock()
        }
    }
}
// Broadcast отправляет сообщение всем подключённым клиентам
func (h *Hub) Broadcast(message interface{}) {
    select {
    case h.broadcast <- message:
    case <-time.After(100 * time.Millisecond):
        fmt.Printf("Канал трансляции хаба заполнен для семинара %s\n", h.workshopID)
    }
}
// Register добавляет клиента в хаб
func (h *Hub) Register(client *Client) {
    h.register <- client
}
// Unregister удаляет клиента из хаба
func (h