Ответ
Основная проблема — состояние гонки (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")
}()