Почему Go и WebSockets — идеальный тандем для real-time?
Представьте себе ситуацию: вы запускаете онлайн-чат, биржевой терминал или многопользовательскую игру. Пользователи ждут мгновенной реакции. Никто не хочет обновлять страницу вручную или ждать 5 секунд, пока придет ответ от сервера. Здесь на сцену выходят WebSockets.
Go (Golang) отлично подходит для этой задачи. Его легковесные горутины (goroutines) позволяют держать десятки тысяч одновременных соединений без просадки производительности. А стандартная библиотека net/http в паре с пакетом gorilla/websocket превращает написание real-time приложений в чистое удовольствие.
В этой статье мы разберемся, как работает WebSocket в Go, напишем простой чат и обсудим best practices. Поехали!
Что такое WebSocket и чем он лучше HTTP?
Если кратко — WebSocket это протокол, который устанавливает постоянное двустороннее соединение между клиентом (браузером) и сервером. В отличие от классического HTTP, где клиент всегда инициирует запрос, WebSocket позволяет серверу самому отправлять данные клиенту в любой момент.
Плюсы очевидны:
- Мгновенная доставка — нет задержек на установку нового соединения.
- Меньше трафика — заголовки отправляются только один раз при handshake.
- Эффективность — одно соединение вместо тысячи HTTP-запросов.
Go отлично справляется с управлением этими соединениями. Вместо того чтобы плодить потоки ОС (что дорого), Go использует горутины. Одна горутина — одно соединение. Тысячи горутин работают на нескольких потоках ОС, потребляя минимум памяти.
Установка и подготовка проекта на Go
Предположим, Go у вас уже установлен. Если нет — brew install go или скачайте с golang.org.
Создадим новый модуль:
mkdir go-websocket-chat
cd go-websocket-chat
go mod init go-websocket-chat
Теперь установим самую популярную библиотеку gorilla/websocket:
go get github.com/gorilla/websocket
Готово. Теперь у нас есть всё для создания real-time приложения на Go.
Пишем WebSocket сервер на Go
Начнем с самого простого — эхо-сервера. Клиент отправляет сообщение, сервер его возвращает. Это поможет понять базовую механику.
Шаг 1: Структура и апгрейд соединения
WebSocket соединение начинается с HTTP-запроса (handshake). Сервер должен «апгрейднуть» его до WebSocket. В Go это делается через http.Hijacker или через удобный метод Upgrader из пакета gorilla/websocket.
Создайте файл main.go:
package main
import (
"fmt"
"log"
"net/http"
"github.com/gorilla/websocket"
)
var upgrader = websocket.Upgrader{
CheckOrigin: func(r *http.Request) bool {
return true // разрешаем любые источники (для разработки)
},
}
func handleWebSocket(w http.ResponseWriter, r *http.Request) {
conn, err := upgrader.Upgrade(w, r, nil)
if err != nil {
log.Print("Ошибка апгрейда:", err)
return
}
defer conn.Close()
// Бесконечный цикл чтения и записи
for {
messageType, message, err := conn.ReadMessage()
if err != nil {
log.Println("Ошибка чтения:", err)
break
}
log.Printf("Получено: %s", message)
// Отправляем то же самое обратно
err = conn.WriteMessage(messageType, message)
if err != nil {
log.Println("Ошибка записи:", err)
break
}
}
}
func main() {
http.HandleFunc("/ws", handleWebSocket)
fmt.Println("Сервер запущен на :8080")
log.Fatal(http.ListenAndServe(":8080", nil))
}
Запустите: go run main.go.
Теперь откройте консоль браузера (F12) и выполните JavaScript код для подключения:
const socket = new WebSocket("ws://localhost:8080/ws");
socket.onopen = () => {
socket.send("Привет, Go!");
};
socket.onmessage = (event) => {
console.log("Ответ:", event.data);
};
Если в консоли появилось «Привет, Go!» — всё работает. Это основа любого real-time приложения на Go.
Создаем многопользовательский чат на Go
Эхо-сервер — это скучно. Давайте сделаем полноценный чат, где сообщения одного клиента видят все остальные. Для этого нам понадобится управлять списком подключений.
Шаг 2: Управление подключениями
Создадим структуру Hub, которая хранит всех активных клиентов и рассылает сообщения.
type Client struct {
hub *Hub
conn *websocket.Conn
send chan []byte
}
type Hub struct {
clients map[*Client]bool
broadcast chan []byte
register chan *Client
unregister chan *Client
}
func newHub() *Hub {
return &Hub{
clients: make(map[*Client]bool),
broadcast: make(chan []byte),
register: make(chan *Client),
unregister: make(chan *Client),
}
}
Теперь реализуем метод Run, который будет слушать каналы и управлять клиентами:
func (h *Hub) Run() {
for {
select {
case client := <-h.register:
h.clients[client] = true
log.Println("Новый клиент подключен")
case client := <-h.unregister:
if _, ok := h.clients[client]; ok {
delete(h.clients, client)
close(client.send)
log.Println("Клиент отключен")
}
case message := <-h.broadcast:
for client := range h.clients {
select {
case client.send <- message:
default:
close(client.send)
delete(h.clients, client)
}
}
}
}
}
Шаг 3: Чтение и запись для каждого клиента
Каждый клиент будет работать в двух горутинах: одна читает из WebSocket и отправляет в broadcast, другая пишет из канала send обратно в WebSocket.
func (c *Client) readPump() {
defer func() {
c.hub.unregister <- c
c.conn.Close()
}()
for {
_, message, err := c.conn.ReadMessage()
if err != nil {
break
}
// Рассылаем всем
c.hub.broadcast <- message
}
}
func (c *Client) writePump() {
defer c.conn.Close()
for message := range c.send {
err := c.conn.WriteMessage(websocket.TextMessage, message)
if err != nil {
break
}
}
}
Шаг 4: Собираем всё вместе
Теперь обновим handleWebSocket: