Ответ
Основная проблема — состояние гонки (data race). Большинство реализаций WebSocket (например, популярная gorilla/websocket) не являются потокобезопасными для записи. Спецификация WebSocket требует, чтобы сообщения отправлялись последовательно, и одновременная запись из нескольких горутин может привести к:
- Повреждению данных: Фрагменты разных сообщений могут перемешаться, делая их нечитаемыми на стороне клиента.
- Панике (Panic): Некоторые библиотеки могут запаниковать при обнаружении конкурентной записи.
- Непредсказуемому поведению: Соединение может быть разорвано или перейти в некорректное состояние.
Решения
Для синхронизации доступа к соединению на запись используется два основных подхода:
1. Мьютекс (Mutex)
Обернуть вызовы записи в мьютекс, чтобы гарантировать, что в каждый момент времени пишет только одна горутина. Это самый простой подход.
import (
"sync"
"github.com/gorilla/websocket"
)
type SafeConn struct {
conn *websocket.Conn
mu sync.Mutex
}
func (sc *SafeConn) WriteJSON(v interface{}) error {
sc.mu.Lock()
defer sc.mu.Unlock()
return sc.conn.WriteJSON(v)
}
func (sc *SafeConn) WriteMessage(messageType int, data []byte) error {
sc.mu.Lock()
defer sc.mu.Unlock()
return sc.conn.WriteMessage(messageType, data)
}
2. Канал-диспетчер (Централизованная запись)
Создается одна выделенная горутина-писатель, которая читает сообщения из канала и последовательно отправляет их в WebSocket. Другие горутины отправляют данные не напрямую в соединение, а в этот канал.
- Плюсы: Не блокирует горутины-отправители. Они быстро отправляют данные в буферизованный канал и продолжают работу.
- Минусы: Немного сложнее в реализации.
// Горутина, отвечающая за запись в соединение
func writer(conn *websocket.Conn, ch <-chan []byte) {
for msg := range ch {
if err := conn.WriteMessage(websocket.TextMessage, msg); err != nil {
// Обработка ошибки, например, логирование и выход
return
}
}
}
// В основной части программы:
// 1. Создаем канал
writeChan := make(chan []byte, 256)
// 2. Запускаем горутину-писателя
go writer(conn, writeChan)
// 3. В других горутинах отправляем сообщения в канал
go func() {
// ... какая-то логика
writeChan <- []byte("Hello from goroutine 1")
}()
go func() {
// ... какая-то логика
writeChan <- []byte("Hello from goroutine 2")
}() Ответ 18+ 🔞
А, ну ты смотри, какие проблемы подкинули! WebSocket, говоришь? И горутины туда же, писать хотят все сразу, как голодные псы на миску. Так вот, слушай сюда, вся соль в этой состоянии гонки (data race), ёпта. Библиотеки эти, типа gorilla/websocket, они же не железобетонные, они как та мартышлюшка на тонкой ветке — если с двух сторон трясти, она в пизду полетит.
Если из нескольких горутин начать в одно соединение писать, то получится полный пиздец, в рот меня чих-пых:
- Данные перемешаются как салат оливье после пятой рюмки. Клиент получит абракадабру и офигеет.
- Паника (Panic) может случиться, библиотека взвоет: "На хуя вы меня одновременно дергаете?!" — и рухнет.
- Непредсказуемость — соединение может просто взять и откинуться, как будто его и не было.
Короче, так делать — это как ебаться без презерватива: вроде весело, но последствия ебать.
Ну и как с этим бороться?
Есть два классических способа, как навести порядок в этом борделе.
1. Мьютекс (Mutex) — Просто и в лоб
Берёшь свой WebSocket-коннект, оборачиваешь в структуру с замком и всё. Кто первый захватил мьютекс — тот и пишет, остальные ждут, как лохи в очереди за халявой.
import (
"sync"
"github.com/gorilla/websocket"
)
type SafeConn struct {
conn *websocket.Conn
mu sync.Mutex // Вот этот наш вышибала
}
func (sc *SafeConn) WriteJSON(v interface{}) error {
sc.mu.Lock() // Захватил замок — ты царь и бог
defer sc.mu.Unlock() // Отпустил, когда всё сделал
return sc.conn.WriteJSON(v)
}
Проще пареной репы. Но есть нюанс: пока одна горутина пишет, остальные тупо стоят и ждут. Для многих сценариев — норм.
2. Канал-диспетчер (Централизованная запись) — Похитрее
А это уже для эстетов. Заводим одну-единственную горутину-писателя. Она сидит, слушает канал и аккуратно, по одному, отправляет сообщения. Все остальные горутины не лезут в коннект, а просто швыряют свои сообщения в этот канал и дальше по своим делам.
- Плюс: Горутины-отправители не блокируются. Отправил в канал — и свободен, как птица.
- Минус: Нужно мозги включать, архитектуру продумывать.
// Эта горутина — наш единственный легальный писатель. Царь и бог записи.
func writer(conn *websocket.Conn, ch <-chan []byte) {
for msg := range ch { // Сидит, ждёт сообщений из канала
if err := conn.WriteMessage(websocket.TextMessage, msg); err != nil {
// Ой, всё. Соединение сдохло. Пора валить.
return
}
}
}
// А в основном коде делаем так:
// 1. Канал создаём, буфером, чтобы не тормозить отправителей
writeChan := make(chan []byte, 256)
// 2. Запускаем нашу царицу-писателя
go writer(conn, writeChan)
// 3. Теперь любая горутина может отправить сообщение БЕЗОПАСНО
go func() {
// ... делаем что-то полезное
writeChan <- []byte("Привет из первой горутины!")
}()
go func() {
// ... делаем что-то ещё
writeChan <- []byte("А это вторая горутина шлёт привет!")
}()
Вот и вся магия. Либо мьютексом по лбу всем, кто не в очередь, либо завести одного ответственного писаку. Выбирай, что для твоего случая больше подходит, а то так и до беды недалеко, ядрёна вошь.