Содержание урока · 11 разделов
- Gorilla websocket установка: go mod init и go get
- Типичная ошибка: go get вне модуля и импорты из чужих туториалов
- websocket.Upgrader: структура, которая решает всё
- CheckOrigin: почему по умолчанию только свой домен
- Буферы: за что отвечают ReadBufferSize и WriteBufferSize
- Поля Upgrader, которые встретятся в курсе
- Кейс: WS-эндпоинт в готовом сервисе за nginx
- Вариация: тот же апгрейд в coder/websocket
- Песочница урока
- Коротко: что запомнить
- Вопросы и ответы
Gorilla websocket установка: go mod init и go get#
Типичный сценарий: у вас уже есть сервис на net/http или на роутере — страницы отдаются, API живёт, и вдруг появляется задача с real-time: чат, уведомления, живой счётчик. Тащить фреймворк ради одной функции не хочется, и хорошо, что не нужно: WebSocket на Go — это одна библиотека поверх того, что уже есть. gorilla/websocket приносит три главных сущности: Upgrader, который проводит рукопожатие, Conn — само соединение, и Dialer — исходящее соединение для тестов и сервер-сервер трафика. Библиотека собрана на чистой стандартной библиотеке: в go.mod проекта появляется ровно одна запись, транзитивных зависимостей нет, а хендлеры остаются обычными http.HandlerFunc. Иными словами, WS-сервер — это всё ещё ваш старый сервер: те же маршруты, тот же ListenAndServe, просто один из хендлеров после апгрейда перестаёт говорить по HTTP. Разберём путь целиком — от go mod init до полей Upgrader, которыми решается, кого впускать.
Начните с чистой папки и модуля. go mod init example.com/ws101 объявляет модуль — без этого go get некуда записывать зависимость; имя модуля — это путь, а не URL, и его не нужно заводить в репозитории. Затем go get github.com/gorilla/websocket подтягивает актуальную версию первой мажорной ветки и пишет её в require go.mod — на момент курса это v1.5.x. Суффиксов в пути импорта нет и не будет: у chi пятая мажорная живёт прямо в пути как /v5, у gorilla путь короткий — github.com/gorilla/websocket, и это частый источник опечаток в туториалах в обратную сторону. В коде пакет импортируется одним именем — websocket — и дальше вы пишете websocket.Upgrader, websocket.Conn, websocket.TextMessage. go.sum получит хэши для воспроизводимой сборки, как у любой зависимости; go mod tidy вычистит лишнее, если вы удалили код. Три команды и одна строка импорта — вся церемония установки.
Три команды до зависимости
GOmkdir ws101 && cd ws101
go mod init example.com/ws101
go get github.com/gorilla/websocketТри команды — и зависимость в go.mod. Модуль обязателен: без go mod init команде go get некуда писать require. Путь импорта короткий — github.com/gorilla/websocket, без суффиксов версии: мажорная ветка у библиотеки первая. В go.sum появятся хэши для воспроизводимости сборки, как и у любой другой зависимости проекта.
Типичная ошибка: go get вне модуля и импорты из чужих туториалов#
Первая же попытка поставить библиотеку регулярно спотыкается на двух граблях, и обе лечатся пониманием, а не переустановкой. Грабля первая: go get запущен в папке без go mod init — инструмент честно отвечает, что go.mod не найден, и ничего не ставит. Модуль — не формальность, а адрес, куда пишется зависимость, поэтому три команды из блока выше выполняются строго в этом порядке. Грабля вторая — копипаст импортов из соседних туториалов: путь с /v5 из урока по chi не скомпилируется с gorilla, и наоборот — короткий путь без версии здесь не ошибка, а норма. Отдельная история — старые статьи: API gorilla стабилен, но примеры эпохи до 2018 года иногда тянут давно удалённые подпакеты. Если сборка ругается на неожиданный путь, сверяйтесь не с памятью, а с go.mod: что записано в require — то и импортируется. И приучите себя к go mod tidy после экспериментов: он убирает зависимости от удалённого кода и не даёт go.mod превратиться в свалку.
websocket.Upgrader: структура, которая решает всё#
Upgrader — конфигурация рукопожатия, и его поля стоит знать до того, как писать код. ReadBufferSize и WriteBufferSize задают размеры внутренних буферов ввода-вывода на соединение; ноль означает дефолт в 4096 байт. HandshakeTimeout — дедлайн на фазу рукопожатия: клиент, приславший GET и замолчавший, не будет держать соединение вечно. CheckOrigin — функция-фильтр, решающая, чужой ли Origin пришёл; дефолт строгий, о нём отдельно ниже. Subprotocols выбирает субпротокол из заголовка Sec-WebSocket-Protocol, EnableCompression включает расширение permessage-deflate для сжатия кадров, а Error позволяет заменить стандартный способ записи HTTP-ошибки при провале апгрейда на свой — например, чтобы логировать причину. Сам апгрейд делает метод Upgrade(w http.ResponseWriter, r *http.Request, responseHeader http.Header): третий аргумент — заголовки, которые хочется добавить в ответ 101 до его отправки, скажем cookie; чаще всего передают nil. Возвращает метод conn — соединение — и ошибку, и обращение с ошибкой здесь нестандартное.
Производственный Upgrader и первый апгрейд
GOvar upgrader = websocket.Upgrader{
ReadBufferSize: 1024,
WriteBufferSize: 1024,
HandshakeTimeout: 10 * time.Second,
CheckOrigin: func(r *http.Request) bool {
return r.Header.Get("Origin") == "https://app.example.com"
},
}
func ws(w http.ResponseWriter, r *http.Request) {
conn, err := upgrader.Upgrade(w, r, nil)
if err != nil {
log.Println("отказ:", err) // клиент уже получил HTTP-ответ
return
}
defer conn.Close()
// дальше — цикл чтения, тема четвёртого урока
}Upgrader объявляют один раз на весь процесс: структура дешёвая по ресурсам, но дорогая по смыслу. Upgrade проводит всё рукопожатие — проверяет метод, заголовки, версию и Origin, отвечает 101 и возвращает conn. При любой неудаче функция сама записывает HTTP-ответ с подходящим кодом ошибки, поэтому хендлеру остаётся залогировать и выйти: второй ответ писать нельзя.
Upgrade требует у ResponseWriter метод Hijack — соединение вынимается из-под net/http. Любая middleware с собственной обёрткой писателя — gzip, счётчик байтов, часть логгеров — ломает апгрейд с ошибкой websocket: response does not implement http.Hijacker; лечение одно: роутить /ws мимо таких звеньев. Вторая ловушка — хендлер, который уже записал тело ответа: 200 уехал, второй статус 101 по тому же соединению не приедет, рукопожатие мертво. Upgrade ставят первым оператором хендлера, до любого Write.
Проверка: ошибка апгрейда
Upgrade вернул ошибку, и хендлер после неё вызвал w.WriteHeader(http.StatusInternalServerError). Что получит клиент?
CheckOrigin: почему по умолчанию только свой домен#
Браузер всегда прикладывает к рукопожатию Origin — адрес страницы, открывшей соединение. У WebSocket нет CORS в HTTP-смысле; вместо него — CheckOrigin. Дефолт gorilla строгий: Origin должен совпасть с Host самого запроса, иначе рукопожатие отклоняется с 403. Логика простая: страница чужого сайта не должна открывать сокет к вашему сервису и читать сообщения от имени залогиненного пользователя — cookies-то браузер приложит автоматически. В реальной разработке дефолт напомнит о себе сразу: фронтенд на localhost:3000, сервер на localhost:8080 — и рукопожатие отвергнуто, нужен свой CheckOrigin, пропускающий локальные адреса. Опасность в другом: в интернете полно советов написать return true и жить дальше. На dev это терпимо, в проде — открытая дверь для чужих страниц: отдельный урок курса про origins разбирает атаку и правильные фильтры, а здесь запомните форму: CheckOrigin — функция из http.Request в bool.
Проверка: два порта на локалке
Фронтенд открыт с http://localhost:3000, сервер на localhost:8080, CheckOrigin не задан. Чем закончится new WebSocket?
Что вернёт Upgrade на POST
Прочитайте код и выберите, что он выведет:
// в upgrader.Upgrade ушёл запрос с методом POST
conn, err := upgrader.Upgrade(w, r, nil)
fmt.Println(conn, err)Буферы: за что отвечают ReadBufferSize и WriteBufferSize#
На каждое соединение gorilla выделяет два буфера — на чтение и на запись — размеров ровно из Upgrader, по умолчанию по 4096 байт каждый. От размера зависит баланс: крупный буфер режет число системных вызовов на больших сообщениях и ускоряет ввод-вывод, но множит память — буферы живут у каждого соединения, пока оно открыто. Грубая прикидка с условием: десять тысяч открытых клиентов при паре буферов по 4096 байт — это порядка восьмидесяти мегабайт только на буферы; при 1024 — десять. Для чата и уведомлений, где сообщения короткие, значения 1024-4096 закрывают потребности; для передачи крупных бинарных кусков крупнее буфер оправдан. Писать в этих буферах вручную не придётся: ReadMessage и WriteMessage из четвёртого урока прячут всю механику. Знать поля стоит по другой причине: при отладке памяти под нагрузкой первые два подозреваемых — размеры буферов Upgrader и количество живых соединений.
Поля Upgrader, которые встретятся в курсе#
- HandshakeTimeout — дедлайн фазы рукопожатия: молчаливый клиент не удержит соединение вечно.
- CheckOrigin — фильтр Origin: дефолт строго свой домен, кастом нужен при раздельных портах.
- Subprotocols — согласование субпротокола из заголовка Sec-WebSocket-Protocol.
- EnableCompression — расширение permessage-deflate: сжатие кадров на лету.
- ReadBufferSize и WriteBufferSize — размеры буферов ввода-вывода на соединение, дефолт 4096.
- Error — своя функция записи HTTP-ошибки при провале апгрейда: лог вместо дефолтного текста.
Апгрейд внутри роутера chi
GOr := chi.NewRouter()
r.Get("/ws", func(w http.ResponseWriter, r *http.Request) {
conn, err := upgrader.Upgrade(w, r, nil)
if err != nil {
return // ответ клиенту уже записан библиотекой
}
defer conn.Close()
// цикл чтения — как в следующем уроке
})
http.ListenAndServe(":8080", r)chi.Router реализует http.Handler, поэтому апгрейд работает без адаптеров: WS-маршрут регистрируется обычным Get рядом с REST-эндпоинтами. Единственная оговорка — стек middleware: слои с собственной обёрткой ResponseWriter надо ставить так, чтобы /ws их не проходил, иначе Hijack не найдётся. Подробно про chi — в отдельном курсе сайта.
Кейс: WS-эндпоинт в готовом сервисе за nginx#
Частая постановка: сервис с REST уже живёт за nginx, и в него добавляют /ws. На локалке всё работает, а в проде соединение умирает через минуту тишины — классика с тремя причинами. Первая: nginx по умолчанию не прокидывает заголовки апгрейда, и рукопожатие даже не начинается; лечится блоком location /ws с proxy_set_header для Upgrade и Connection плюс proxy_http_version 1.1. Вторая: proxy_read_timeout по умолчанию — шестьдесят секунд, и молчаливое WS-соединение прокси рвёт, хотя оба конца живы; значение поднимают, а правильнее — вешают ping-механизм из шестого урока, чтобы кадры ходили чаще таймаута. Третья: gzip-слой или обёртка ResponseWriter в цепочке приложения ломает Hijack — об этом была врезка выше. Полезно понять общий принцип: для WS-маршрута весь путь от браузера до Go-процесса должен быть прозрачным для апгрейда, и каждый прокси в цепочке проверяется отдельно. Этот чек-лист я прогоняю перед любым демо real-time: девять из десяти жалоб «сервер падает» оказываются прокси.
Типичная dev-связка: фронтенд на :3000 поднимает Vite или webpack, сервер Go — на :8080. Дефолтный CheckOrigin такое рукопожатие отвергнет, поэтому в разработке пишут функцию, пропускающую localhost, — а на проде фронтенд и WS обычно живут за одним доменом и одним nginx, и вопрос исчезает вместе с CORS-настройками.
Вариация: тот же апгрейд в coder/websocket#
Для полноты картины — как то же рукопожатие выглядит в альтернативной библиотеке. Пакет coder/websocket не имеет Upgrader: функцией Accept(w, r) вы принимаете соединение и сразу получаете conn, жизнью соединения управляет контекст запроса, а чтение построено на io.Reader вместо ReadMessage — сообщение приходит потоком, который вы читаете сами. Концептуально всё то же самое: тот же HTTP-запрос, тот же ответ 101, то же требование одной горутины-писателя на соединение. Меняется стиль: gorilla держит состояние в структуре Conn с полями-дедлайнами, coder — в контекстах и ридерах, что ближе к современному Go, но непривычнее после учебников. Мой практический критерий не меняется от направления: если в проекте уже есть код на gorilla или команда его знает — переезд не оправдан, API учится за вечер; для нового сервиса в ctx-first стиле честнее coder. Остальной курс идёт на gorilla — по нему написано больше всего, а навык переносится на любую из библиотек.
Соберите производственный Upgrader
Заполните три пропуска: поле-фильтр Origin, единицы дедлайна и метод рукопожатия.
От пустой папки до апгрейда
Расставьте шаги установки в порядке выполнения: от создания папки до первого апгрейда в коде.
Строки
Ваша программа
Нажимайте на строки слева, чтобы собрать программу
Карточки: установка и Upgrader
Дальше — четвёртый урок: полный эхо-сервер, где под апгрейдером появится цикл ReadMessage-WriteMessage и первая реальная жизнь соединения. Если что-то из протокола рукопожатия осталось туманным, вернитесь ко второму уроку — там каждая проверка Upgrade разобрана на уровне заголовков. Практический совет напоследок: апгрейд рождает долгоживущую горутину, поэтому перед хабом стоит освежить конкурентность в продвинутом курсе Go — там каналы, select и завершение горутин. Запускать код этого урока в песочнице бессмысленно — рукопожатие требует живого TCP и клиента напротив; поднимайте локальный Go, а карта всех уроков лежит на странице курса.
Песочница урока
Редактируйте код и запускайте его прямо здесь — интерпретатор работает в браузере, ничего устанавливать не нужно. За каждый запуск — +2 XP.
Запустите код, чтобы увидеть вывод.
Коротко: что запомнить
- Установка — go get github.com/gorilla/websocket: один пакет без транзитивных зависимостей, путь импорта без суффиксов версии.
- Upgrader объявляют один раз: буферы, HandshakeTimeout, CheckOrigin и остальные поля настраивают под деплой.
- Upgrade проверяет метод, заголовки, версию и Origin, отвечает 101 и возвращает conn; при ошибке HTTP-ответ пишет сам.
- Дефолтный CheckOrigin пропускает только совпадение Origin с Host — для dev на двух портах нужен свой фильтр.
- Middleware с обёрткой ResponseWriter ломает апгрейд: у писателя должен быть Hijack, а Upgrade — первая строка хендлера.
- ReadBufferSize и WriteBufferSize — компромисс между числом syscalls и памятью на каждое соединение.
Домашнее задание
Разверните пустой модуль и поставьте gorilla/websocket командами из урока. Соберите Upgrader с HandshakeTimeout и CheckOrigin, который пропускает только ваш Origin, и хендлером, где Upgrade — первая строка. Поднимите сервер на :8080 и убедитесь curl-ом, что обычный GET на /ws получает ошибку, а не 101: рукопожатие без заголовков апгрейда обязано провалиться.
Личные заметкиПусто
Вопросы и ответы по уроку
Почему у gorilla нет /v5 в пути импорта, как у chi?
Семантическое версионирование Go требует суффикс только со второй мажорной версии. gorilla/websocket живёт в первой: путь короткий — github.com/gorilla/websocket. Если однажды выйдет вторая, импорт станет github.com/gorilla/websocket/v2, и старый код заметит это по ошибке компиляции.
Можно ли обойтись без Upgrader и написать рукопожатие руками?
Технически да: проверить заголовки, посчитать Sec-WebSocket-Accept, сделать Hijack. Это сотни строк точной работы с протоколом, включая маскирование и фрагментацию кадров. Upgrader существует именно поэтому; руками рукопожатие пишут разве что ради понимания, как во втором уроке.
Что выбрать — gorilla/websocket или coder/websocket?
gorilla — самый распространённый API: его знает каждый туториал и большинство собеседований; coder/websocket современнее — контексты и io.Reader. Для первого real-time проекта и этого курса gorilla честнее: навык переносится, а переезд на coder потом занимает вечер.
Термины урока
Ключевые понятия из этого урока — короткие определения в глоссарии. Наведите на термин, чтобы вспомнить, откройте карточку для подробностей и ссылки на практику.
Изучайте дальше
Следующая ступень учебного пути
RabbitMQ в Go: очереди сообщений на amqp091-go
Интерактивный курс RabbitMQ в Go: обменники, очереди, ack, prefetch, dead letter и RPC. 25 уроков amqp091-go с примерами кода и итоговым проектом.
- Подключаться к RabbitMQ из Go и управлять connection и channel
- Объявлять очереди, обменники и привязки с нужными свойствами
- Публиковать и потреблять сообщения с ручными ack
- Настраивать prefetch и справедливое распределение между воркерами