Содержание урока · 13 разделов
- gin прокси: зачем фреймворку чужие ответы
- httputil.ReverseProxy: Director и Rewrite
- Прокси на группе: round-robin и выбор цели
- Поток без памяти: io.Copy и FlushInterval
- c.Stream: шаговая функция gin
- SSE: протокол событий текстом
- Заголовки по пути: hop-by-hop и X-Forwarded-*
- Точки вмешательства ReverseProxy
- Грабли: буферизация, таймауты и медленные клиенты
- Шесть правил прокси и стриминга
- Песочница урока
- Коротко: что запомнить
- Вопросы и ответы
gin прокси: зачем фреймворку чужие ответы#
Обратный прокси — сервер, который принимает запрос, сам ходит за ответом к бэкенду и возвращает его клиенту как свой. Зачем это нужно, когда есть nginx? Во-первых, единая дверь: одно доменное имя, один TLS, а за ним хоть десять сервисов — фронтенд не хочет знать их адреса и CORS-танцы исчезают сами собой, как мы обсуждали в уроке про CORS. Во-вторых, место для логики: у nginx язык конфигов, а у вас — Go. Выбор бэкенда по клейму из JWT, тарифу пользователя или весовой доле — это код, а не config. В-третьих, агрегация: типичный BFF (backend for frontend) собирает данные с нескольких сервисов, и проксирование — его базовый приём. gin прокси здесь удобен тем, что это обычный хендлер: вокруг него работают те же группы, звенья, конверт ошибок и логирование, что и вокруг остального приложения. Важное уточнение по ресурсам: прокси не читает ответы целиком — он копирует поток из сети в сеть, поэтому бэкенд может отдать гигабайтный файл, а память прокси останется на уровне буфера в десятки килобайт. Как это устроено — тема второй половины урока.
httputil.ReverseProxy: Director и Rewrite#
Основа — httputil.ReverseProxy из стандартной библиотеки: структура с несколькими точками вмешательства. Transport — чем ходить на бэкенд (по умолчанию http.DefaultTransport); ModifyResponse — правка ответа до отправки клиенту; ErrorHandler — что ответить, когда бэкенд недоступен; FlushInterval — как часто выталкивать буферы клиенту; ErrorLog — куда жаловаться. Главный выбор — функция-маршрутизатор запроса, и тут два поколения API. Легаси-путь — Director func(http.Request): вам дают исходящий запрос, вы правите URL.Scheme, URL.Host, URL.Path — как хотите. Современный путь, с Go 1.20, — Rewrite func(httputil.ProxyRequest): в структуре лежат In (запрос клиента, только чтение) и Out (запрос к бэкенду), плюс два готовых метода: SetURL(target) проставляет схему, хост и путь цели с сохранением query, а SetXForwardedFor() заполняет заголовки X-Forwarded-For, X-Forwarded-Host и X-Forwarded-Proto. Разница глубже синтаксиса: перед Rewrite прокси стирает клиентские X-Forwarded-* — защита от подделки, а перед Director не стирает; и после Director прокси сам дописывает IP клиента к существующему X-Forwarded-For, а в режиме Rewrite ничего не ставит без вашего ведома. Шорткат NewSingleHostReverseProxy(target) собирает Director за вас: константа цели, склейка путей, сохранение query — идеально для первого знакомства.
Минимальный прокси: NewSingleHostReverseProxy
GOpackage main
import (
"net/http"
"net/http/httputil"
"net/url"
"github.com/gin-gonic/gin"
)
func main() {
target, _ := url.Parse("http://127.0.0.1:9000")
proxy := httputil.NewSingleHostReverseProxy(target)
r := gin.New()
r.Use(gin.Logger(), gin.Recovery())
// Всё с /api уходит на бэкенд, префикс отрезаем.
r.Any("/api/*path", func(c *gin.Context) {
c.Request.URL.Path = c.Param("path")
proxy.ServeHTTP(c.Writer, c.Request)
})
r.Run(":8080")
}NewSingleHostReverseProxy вернул структуру с готовым Director-ом: цель константа, пути склеиваются. В хендлере мы подменяем путь запроса: wildcard ловит /api/todos, а бэкенд ждёт /todos — c.Param("path") отдаёт хвост без префикса. Метод Any прокидывает все HTTP-глаголы сразу. ServeHTTP принимает стандартные ResponseWriter и Request — gin-писатель реализует первый, поэтому прокси встаёт в хендлер без обёрток.
Проверка: судьба X-Forwarded-For
Клиент шлёт X-Forwarded-For: 203.0.113.7 и попадает на NewSingleHostReverseProxy. Что окажется в этом заголовке на бэкенде?
Прокси на группе: round-robin и выбор цели#
Один бэкенд — разминочный круг. Реальный шлюз выбирает цель: реплики крутятся по кругу (round-robin), тарифный план решает, кому достанется тяжёлый запрос, апи-версия в заголовке развозит по версиям сервиса. Механика та же, но прокси теперь не одна, а массив: создаём пул один раз при старте — структуры дешёвые, а Director/Rewrite-замыкание хранит свою цель. Счётчик — atomic.Uint64: Add(1) атомарен, горутины не дерутся, а index по модулю длины пула даёт честный круг. Хендлер на группе из урока 10 принимает /v1/*path, отрезает префикс и передаёт выбранному прокси. Если нужен префикс на бэкенде — не отрезайте, SetURL склеит путь цели с хвостом. Для sticky-сессий (клиент всегда к той же реплике) ключом берут cookie из урока про сессии или hash идентификатора — round-robin ломает липкость, но для stateless-API это неважно. И помните: у каждого прокси из пула свои ErrorHandler и ModifyResponse — собирайте их фабрикой, чтобы не копипастить.
Пул прокси с Rewrite: round-robin по репликам
GOtargets := []*url.URL{
{Scheme: "http", Host: "127.0.0.1:9001"},
{Scheme: "http", Host: "127.0.0.1:9002"},
{Scheme: "http", Host: "127.0.0.1:9003"},
}
var next atomic.Uint64
proxies := make([]*httputil.ReverseProxy, len(targets))
for i, t := range targets {
t := t // захват переменной цикла — своя цель на прокси
proxies[i] = &httputil.ReverseProxy{
Rewrite: func(pr *httputil.ProxyRequest) {
pr.SetURL(t) // схема, хост, склейка пути
pr.SetXForwardedFor() // XFF/XFH/XFP заново
},
ModifyResponse: func(resp *http.Response) error {
resp.Header.Set("X-Upstream", t.Host)
return nil
},
ErrorHandler: func(w http.ResponseWriter,
r *http.Request, err error) {
w.WriteHeader(http.StatusBadGateway)
},
}
}
r.Any("/v1/*path", func(c *gin.Context) {
idx := int(next.Add(1)-1) % len(proxies)
c.Request.URL.Path = c.Param("path")
proxies[idx].ServeHTTP(c.Writer, c.Request)
})Пул собирается на старте: три структуры, у каждой своя цель в замыкании. Rewrite — современный режим: SetURL ставит цель и склеивает путь с сохранением query, SetXForwardedFor затирает клиентские X-Forwarded-* и заполняет честными. ModifyResponse ставит отладочный X-Upstream, ErrorHandler отдаёт 502 — в бою тут будет JSON-конверт из урока про ошибки. Хендлер — три строки: выбрал реплику атомарным счётчиком, отрезал префикс, отдал прокси.
Проверка: куда уйдут пять запросов
Пул из двух реплик: 9001 и 9002. next — atomic.Uint64 с нуля; хендлер берёт idx = int(next.Add(1)-1) % len(proxies). Пять запросов подряд — куда они уйдут?
Поток без памяти: io.Copy и FlushInterval#
Почему прокси не боится гигабайтных ответов? Потому что он не читает их в память. ReverseProxy копирует тело через io.Copy: буфер 32 КиБ, прочитали — записали клиенту, забыли. Ручной хендлер повторяет ту же логику в три строки: http.Get (или клиент с таймаутами), скопировать заголовки, io.Copy из resp.Body в c.Writer. Буферизация в памяти нужна только если вы реально хотите править тело — тогда вы платите RAM-ом за размер ответа и осознанно, а не случайно. Вторая сторона — своевременность. Байты, записанные в ResponseWriter, по умолчанию не обязаны покидать процесс немедленно: сетевой слой собирает их в пакеты. Для стриминга нужен Flush — явное «выталкивай сейчас». gin-писатель реализует http.Flusher, поэтому c.Writer.Flush() доступен всегда. У ReverseProxy за это отвечает FlushInterval: с Go 1.16 он сам ставит мгновенный flush для ответов с Content-Type text/event-stream и без известной Content-Length — стриминг работает из коробки; для обычных ответов прокси не торопится, и если бэкенд шлёт данные кусками, задайте FlushInterval: 100 * time.Millisecond. Отдача файлов с диска — та же идея без сети: c.File из урока про загрузку файлов льёт их стандартным FileServer-ом.
Ручной потоковый хендлер: строка за строкой
GOr.GET("/logs", func(c *gin.Context) {
upstream, err := http.Get("http://127.0.0.1:9000/logs.txt")
if err != nil {
c.JSON(http.StatusBadGateway, gin.H{
"error": "upstream недоступен"})
return
}
defer upstream.Body.Close()
c.Writer.Header().Set("Content-Type",
"text/plain; charset=utf-8")
c.Writer.WriteHeader(upstream.StatusCode)
sc := bufio.NewScanner(upstream.Body)
for sc.Scan() {
fmt.Fprintln(c.Writer, sc.Text())
c.Writer.Flush() // строка уходит немедленно
}
})Статус и Content-Type берём с бэкенда — клиент узнаёт правду об апстриме. Scanner читает строку, Fprintln пишет её клиенту, Flush выталкивает — хвост лога течёт в браузер в реальном времени, память прокси при этом нулевая. Помните потолок Scanner: строка длиннее 64 КиБ уронит скан — для произвольных данных берите io.Copy с FlushInterval, для строк — bufio.Reader.ReadString.
c.Stream: шаговая функция gin#
Для кусочных ответов у gin есть готовый метод: c.Stream(step func(w io.Writer) bool) bool. Контракт простой: gin крутит цикл, на каждом обороте зовёт вашу step, а после каждого вызова сам делает Flush — выталкивание встроено, забывать не надо. step пишет в writer очередной кусок и возвращает решение: true — продолжаем, false — поток закончен. Выход из цикла происходит и без вашего участия: gin слушает CloseNotify соединения и прекращает цикл, когда клиент ушёл — сочинять свой детектор отключения не нужно. Внутри step обычно стоит select: данные из канала, тикер времени, или Context.Done() запроса — контекст отменяется при разрыве, и это второй способ узнать об уходе клиента. Шаговая модель закрывает три кейса: деградированный long-polling, генерация данных на лету (курс валют раз в секунду) и — главный кейс — SSE, до которого дойдём через блок. Против бесконечного цикла с ручным Flush плюс минус очевидны: c.Stream короче, не забывает Flush и корректно закрывается, а вы остаётесь с одной строчкой логики на шаг.
Курс валют шагом: c.Stream и тикер
GOr.GET("/rate", func(c *gin.Context) {
c.Writer.Header().Set("Content-Type", "text/plain")
c.Stream(func(w io.Writer) bool {
select {
case <-time.After(5 * time.Second):
rate := fetchRate() // ход на бэкенд
fmt.Fprintf(w, "курс: %s\n", rate)
return true // следующий шаг через 5 секунд
case <-c.Request.Context().Done():
return false // клиент отключился
}
})
})Каждый шаг ждёт тикера, пишет строку и возвращает true — c.Stream сама делает Flush после шага, клиент получает курс каждые пять секунд. Context.Done закрывает поток при разрыве соединения. time.After внутри цикла аллоцирует таймер на шаг; для долгоживущих потоков экономнее time.NewTicker с defer ticker.Stop.
Что увидит клиент при первом false
Прочитайте код и выберите, что он выведет:
c.Stream(func(w io.Writer) bool {
fmt.Fprint(w, "раз ")
return false
})SSE: протокол событий текстом#
Server-Sent Events — стандарт односторонних потоков от сервера к браузеру поверх обычного HTTP. Контент-тип text/event-stream, формат — текст: строка data: несёт полезную нагрузку, event: задаёт имя события, id: — идентификатор для восстановления, retry: — интервал переподключения в миллисекундах; пустая строка означает «событие собрано, отправляй». Два data: подряд склеиваются переносом — многострочные сообщения соберутся сами. На клиенте EventSource: конструктор с URL, обработчики onmessage и addEventListener по имени события — браузер сам переподключается при обрыве, сам шлёт Last-Event-ID с последним полученным id, и сервер может дозировать пропущенное. Всё это живёт на обычном HTTP: прокси, балансировщики и gzip-уровни видят стандартный ответ, а не экзотический протокол. В gin SSE собирается из уже знакомых деталей: заголовки text/event-stream и Cache-Control: no-cache, канал с событиями, c.Stream с шагом, а c.SSEvent(event, obj) пишет форматированное событие целиком — имя, data и перевод строк по стандарту. Двусторонний обмен и бинарные данные — территория WebSocket из урока 20-websocket: там апгрейд соединения и gorilla/websocket, здесь — чистый HTTP, который проходит через любой прокси.
SSE-хаб: каналы и broadcast
GOtype hub struct {
mu sync.Mutex
clients map[chan string]struct{}
}
func (h *hub) add() chan string {
ch := make(chan string, 16)
h.mu.Lock()
h.clients[ch] = struct{}{}
h.mu.Unlock()
return ch
}
func (h *hub) remove(ch chan string) {
h.mu.Lock()
delete(h.clients, ch)
h.mu.Unlock()
}
func (h *hub) broadcast(msg string) {
h.mu.Lock()
defer h.mu.Unlock()
for ch := range h.clients {
select {
case ch <- msg:
default: // буфер полон — медленный клиент пропущен
}
}
}
r.GET("/events", func(c *gin.Context) {
ch := hub.add()
defer hub.remove(ch)
c.Writer.Header().Set("Content-Type", "text/event-stream")
c.Writer.Header().Set("Cache-Control", "no-cache")
c.Stream(func(w io.Writer) bool {
select {
case msg := <-ch:
c.SSEvent("message", msg)
return true
case <-c.Request.Context().Done():
return false
}
})
})
r.POST("/push", func(c *gin.Context) {
var body struct {
Text string `json:"text"`
}
if err := c.ShouldBindJSON(&body); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
hub.broadcast(body.Text)
c.Status(http.StatusAccepted)
})Хаб — карта каналов под мьютексом: add при подключении, remove через defer при любом выходе из хендлера, broadcast раскладывает сообщение всем. default в broadcast — защита от медленного подписчика: переполненный буфер лучше пропустить, чем заморозить всех. Эндпоинт /events ставит заголовки SSE и живёт в c.Stream до разрыва. POST /push публикует событие из любого клиента — bind и конверт ошибок стандартные.
Событие на проводе
Прочитайте код и выберите, что он выведет:
c.SSEvent("price", "100")Заголовки по пути: hop-by-hop и X-Forwarded-*#
Прокси не пересылает всё подряд. Стандарт делит заголовки на end-to-end и hop-by-hop: вторые описывают связь между соседними узлами и на следующем хопе бессмысленны — ReverseProxy вырезает Connection, Keep-Alive, TE, Trailer, Transfer-Encoding, Upgrade и Proxy- до похода на бэкенд. Именно поэтому заголовок Upgrade из SSE не ломает проксирование, а WebSocket через ReverseProxy без специальных манипуляций не пройдёт — для апгрейда нужен туннель, о нём поговорим в уроке про WebSocket. Вторая группа — навигационные X-Forwarded-For, X-Forwarded-Host, X-Forwarded-Proto: бэкенду нужно знать настоящий адрес клиента, оригинальный хост и схему. Здесь действует правило из урока про лимиты: верить им можно только от доверенных прокси — иначе первый пришедший напишет себе любой X-Forwarded-For. Режим Rewrite решает это радикально: стирает клиентские X-Forwarded- перед Rewrite-функцией, и SetXForwardedFor заполняет их заново от реального RemoteAddr. Если ваш gin-шлюз сам стоит за nginx, не забудьте про SetTrustedProxies — иначе ClientIP на шлюзе покажет nginx, и per-IP лимиты из урока 18 слипнутся в одно ведро. Host бэкенду проставляет SetURL; если нужно сохранить оригинальный — ведро в Out.Host остаётся пустым, а X-Forwarded-Host унесёт правду.
Точки вмешательства ReverseProxy#
| Точка | Когда вызывается | Что там делать |
|---|---|---|
| Rewrite (современный) | до похода на бэкенд, после вырезания hop-by-hop и X-Forwarded-* | SetURL к цели, SetXForwardedFor, правка пути и заголовков запроса |
| Director (легаси) | до похода на бэкенд, X-Forwarded-* не стираются | правка URL.Request цели; для нового кода предпочитайте Rewrite |
| ModifyResponse | получен ответ от бэкенда, до отправки клиенту | добавить/убрать заголовки, подменить статус, посчитать метрику |
| ErrorHandler | ошибка похода на бэкенд: таймаут, refusal, сбой транспорта | конверт ошибок клиенту, метрика сбоя, аккуратный 502 |
| FlushInterval | во время копирования тела | периодический flush для стриминга; text/event-stream и ответы без длины флешатся сами |
Проверка: бэкенд умер
Реплика за прокси перезапускается, соединение отваливается по ошибке транспорта. ErrorHandler не задан. Что получит клиент?
Грабли: буферизация, таймауты и медленные клиенты#
Первый грабль — невидимая буферизация на пути. Ваш SSE-эндпоинт честно шлёт данные раз в секунду, а браузер молчит: виноват не gin, а промежуточный слой. Собственный gzip-звено на группе собирает тело в буфер — стриминговым маршрутам нужен обход; nginx по умолчанию буферизует проксированные ответы — лечится X-Accel-Buffering: no в ответе бэкенда или proxy_buffering off в конфиге. Диагностика проста: curl видит поток, браузер — нет, значит буферизует кто-то посередине. Второй грабль — таймауты http.Server: WriteTimeout, поставленный на весь сервер, обрежет любой поток длиннее его значения — соединение молча закроется через N секунд. Для серверов со стримингом WriteTimeout обнуляют и контролируют дедлайны точечно через контекст запроса. Третий — таймауты бэкенда: дефолтный Transport ждёт вечно, зависшая реплика повесит и хендлер прокси; настраивайте ResponseHeaderTimeout и общий дедлайн на клиенте. Четвёртый — медленные подписчики SSE: broadcast с default-веткой теряет события для переполненного канала, но сохраняет остальных; очереди без границ в памяти — путь к OOM. Пятый — дублирование лимитов: лимит запросов gin на шлюзе защищает бэкенды бесплатно, не забудьте поставить звено из урока 18 перед прокси-группой, а конверт ошибок взять из урока 17-oshibki.
Шесть правил прокси и стриминга#
- Пул прокси собирается один раз на старте: цель живёт в замыкании Rewrite, хендлер только выбирает индекс и отрезает префикс.
- Rewrite вместо Director для нового кода: SetURL и SetXForwardedFor, стирание клиентских X-Forwarded-* — встроенная защита от подделки.
- Прокси не копит тело: io.Copy кусками по 32 КиБ; буферизация в памяти — осознанное решение за RAM, а не дефолт.
- c.Stream сама делает Flush после каждого шага и ловит уход клиента через CloseNotify; step вернул false — поток закончен штатно.
- SSE — text/event-stream, no-cache, c.SSEvent на канал; переподключение и Last-Event-ID — забота EventSource, а не вашего кода.
- Буферизация посередине и WriteTimeout режут потоки тихо: обходите gzip на стриминговых группах, X-Accel-Buffering: no, дедлайны через контекст, ErrorHandler — для честного 502-конверта.
Соберите SSE-эндпоинт
Допишите поток событий: контент-тип, вход в c.Stream, публикацию события и реакцию на разрыв.
Соберите жизнь одного SSE-сообщения
Клиент смотрит ленту событий. Разложите путь сообщения: от подписки до тихого разрыва.
Строки
Ваша программа
Нажимайте на строки слева, чтобы собрать программу
Карточки: прокси и стриминг
Поднимите бэкенд с /logs.txt и /events на :9000, шлюз из урока на :8080 — и гоняйте curl: curl localhost:8080/api/logs покажет чужой лог как свой ответ; curl -N localhost:8080/events в одном терминале и curl -X POST localhost:8080/push -d '{"text":"привет"}' в другом — событие приедет мгновенно, флаг -N отключает буферизацию самого curl; убейте бэкенд — получите пустой 502, допишите ErrorHandler и увидите свой конверт. Проверьте заголовки: curl -i покажет X-Upstream от ModifyResponse и стёртые X-Forwarded-For; поставьте лимит-звено из урока 18 перед группой /v1 и убедитесь, что оно режет трафик до прокси. В браузерной песочнице сайта сетевой код не исполняется, поэтому интерактивы здесь quiz, predict, fill, order и флешкарты, а примеры живут в локальном go run, как и весь курс gin. Дальше — 20-websocket: gorilla/websocket, апгрейд соединения и двусторонний чат — SSE умеет только шептать в одну сторону, а WebSocket разговорит обе.
Песочница урока
Редактируйте код и запускайте его прямо здесь — интерпретатор работает в браузере, ничего устанавливать не нужно. За каждый запуск — +2 XP.
Запустите код, чтобы увидеть вывод.
Коротко: что запомнить
- Обратный прокси на gin — обычный хендлер: httputil.ReverseProxy перекладывает запрос и ответ, память не растёт от размера тела, потому что копирование идёт потоками по 32 КиБ.
- Rewrite — современная точка маршрутизации: SetURL к цели, SetXForwardedFor с автоматическим стиранием клиентских X-Forwarded-*; Director — легаси с дописыванием IP к существующему списку.
- Пул реплик — массив прокси, собранный на старте, и атомарный счётчик с модулем: round-robin в три строки хендлера, ModifyResponse и ErrorHandler — свои у каждого прокси.
- Стриминг gin держится на Flush: gin-писатель реализует http.Flusher, c.Stream делает Flush после каждого шага и ловит уход клиента, FlushInterval заведует своевременностью у ReverseProxy.
- SSE — обычный HTTP с text/event-stream: data:, event:, id:, retry:, пустая строка-терминатор; EventSource сам переподключается и шлёт Last-Event-ID, серверу остаётся хаб каналов и c.SSEvent.
- Тихие убийцы потоков — буферизация посередине и WriteTimeout: обход gzip, X-Accel-Buffering: no, дедлайны через контекст, ErrorHandler с конвертом вместо пустого 502.
Домашнее задание
Соберите шлюз из трёх реплик на :9001-:9003. Пул httputil.ReverseProxy с Rewrite: SetURL, SetXForwardedFor, X-Upstream в ModifyResponse и ErrorHandler с конвертом ошибок UPSTREAM_UNAVAILABLE и 502. Группа /v1 с round-robin на atomic.Uint64 и отрезанием префикса. Рядом SSE-эндпоинт /events: хаб с каналами под мьютексом, буфер 16, broadcast с default-веткой, заголовки text/event-stream и no-cache, c.Stream с c.SSEvent и Context.Done. POST /push публикует через bind. Проверьте: пять запросов уходят строго по кругу 9001-9002-9003; curl -N /events показывает событие мгновенно после /push; Ctrl-C в curl убирает канал из хаба; убитая реплика отдаёт ваш JSON-конверт, а не пустой 502. Убедитесь, что gzip-звено, если появилось, обходит группу /events, и что WriteTimeout у сервера либо нулевой, либо больше максимального времени жизни потока.
Личные заметкиПусто
Вопросы и ответы по уроку
Rewrite или Director для нового кода?
Rewrite, если минимальная версия Go позволяет: он получает ProxyRequest с разделением In/Out, SetURL и SetXForwardedFor делают типовые вещи, а клиентские X-Forwarded-* стираются автоматически — спуфинг заголовков от клиента отсечён на уровне библиотеки. Director остаётся для совместимости и тонких случаев, где нужна правка именно *http.Request, но для него придётся вручную думать о доверии к XFF.
Чем c.Stream лучше ручного цикла с Flush?
Тремя вещами: Flush после каждого шага встроен и о нём нельзя забыть; выход по уходу клиента уже реализован через CloseNotify; контракт step true/false делает код читаемым. Ручной цикл нужен, когда шаг и выталкивание должны разойтись — например, сканирование большого тела с Flush раз в мегабайт.
Почему браузер не видит SSE-поток, а curl видит?
Классика буферизации посередине: gzip-звено на группе собирает тело, nginx буферизует проксированный ответ, антивирус на клиенте любит перемешивать потоки. Снимите gzip со стриминговой группы, поставьте X-Accel-Buffering: no или proxy_buffering off — и проверьте заголовок Content-Type: браузер требует точного text/event-stream, с charset-суффиксом EventSource может не стартовать.
SSE или WebSocket для чата?
Односторонний поток сервера — SSE: HTTP, автопереподключение, Last-Event-ID, ноль зависимостей. Двусторонний обмен, бинарные кадры, частые сообщения от клиента — WebSocket: апгрейд соединения, gorilla/websocket и отдельный разговор про прокси, потому что Upgrade-заголовок hop-by-hop и через ReverseProxy не пролезает без настройки. Лента уведомлений — SSE; чат, игра, совместное редактирование — WebSocket из следующего урока.
Термины урока
Ключевые понятия из этого урока — короткие определения в глоссарии. Наведите на термин, чтобы вспомнить, откройте карточку для подробностей и ссылки на практику.
Сравните с альтернативами
Практический вопрос после первых уроков: а почему именно эта библиотека? Мы разобрали конкурентов по одним критериям, с кодом одной задачи рядом и персональным подбором под ваши приоритеты.
Изучайте дальше
Следующая ступень учебного пути
Echo: минималистичный быстрый фреймворк
Курс Echo для Go: маршруты, middleware, валидация, JWT, SSE, тестирование и деплой. 25 уроков с примерами и итоговым API-проектом.
- Строить REST API на Echo
- Использовать типизированные запросы/ответы (Request/Response)
- Управлять ошибками через echo.HTTPError
- Писать middleware, включая JWT-аутентификацию