WebSockets permiten comunicación bidireccional persistente sobre una conexión HTTP actualizada. Go tiene dos librerías consolidadas para esto: gorilla/websocket, la más veterana y completa, y nhooyr.io/websocket, más moderna y con soporte nativo para context.Context.
Servidor básico con gorilla/websocket
package main
import (
"fmt"
"log"
"net/http"
"github.com/gorilla/websocket"
)
var upgrader = websocket.Upgrader{
ReadBufferSize: 1024,
WriteBufferSize: 1024,
CheckOrigin: func(r *http.Request) bool {
return true // en producción valida el origen
},
}
func wsHandler(w http.ResponseWriter, r *http.Request) {
conn, err := upgrader.Upgrade(w, r, nil)
if err != nil {
log.Println("error de upgrade:", err)
return
}
defer conn.Close()
for {
mt, msg, err := conn.ReadMessage()
if err != nil {
if websocket.IsUnexpectedCloseError(err, websocket.CloseGoingAway) {
log.Println("conexión cerrada inesperadamente:", err)
}
break
}
log.Printf("recibido: %s", msg)
if err := conn.WriteMessage(mt, msg); err != nil { // eco
break
}
}
}
func main() {
http.HandleFunc("/ws", wsHandler)
log.Fatal(http.ListenAndServe(":8080", nil))
}
Hub para broadcast a múltiples clientes
El patrón hub centraliza la gestión de conexiones y el envío de mensajes a todos los clientes conectados. Es el núcleo de cualquier chat o sala de eventos en tiempo real:
type Hub struct {
clients map[*websocket.Conn]bool
broadcast chan []byte
register chan *websocket.Conn
unregister chan *websocket.Conn
}
func newHub() *Hub {
return &Hub{
clients: make(map[*websocket.Conn]bool),
broadcast: make(chan []byte, 256),
register: make(chan *websocket.Conn),
unregister: make(chan *websocket.Conn),
}
}
func (h *Hub) run() {
for {
select {
case conn := <-h.register:
h.clients[conn] = true
case conn := <-h.unregister:
if _, ok := h.clients[conn]; ok {
delete(h.clients, conn)
conn.Close()
}
case msg := <-h.broadcast:
for conn := range h.clients {
if err := conn.WriteMessage(websocket.TextMessage, msg); err != nil {
conn.Close()
delete(h.clients, conn)
}
}
}
}
}
// En el handler: registra la conexión y lee mensajes
func wsHandlerHub(hub *Hub, w http.ResponseWriter, r *http.Request) {
conn, _ := upgrader.Upgrade(w, r, nil)
hub.register <- conn
defer func() { hub.unregister <- conn }()
for {
_, msg, err := conn.ReadMessage()
if err != nil { break }
hub.broadcast <- msg
}
}
Reconexión con backoff exponencial en el cliente
func conectarConRetry(url string) {
espera := time.Second
for {
conn, _, err := websocket.DefaultDialer.Dial(url, nil)
if err != nil {
log.Printf("error de conexión: %v; reintentando en %s", err, espera)
time.Sleep(espera)
espera = min(espera*2, 30*time.Second) // cap en 30s
continue
}
espera = time.Second // reset al reconectar
log.Println("conectado")
manejarConexion(conn)
conn.Close()
}
}
func min(a, b time.Duration) time.Duration {
if a < b { return a }
return b
}
Keepalive con ping/pong
Las conexiones WebSocket ociosas pueden ser cortadas por proxies o firewalls. El mecanismo ping/pong mantiene la conexión viva y detecta desconexiones reales:
const pingPeriod = 30 * time.Second
const pongWait = 60 * time.Second
func keepAlive(conn *websocket.Conn) {
conn.SetReadDeadline(time.Now().Add(pongWait))
conn.SetPongHandler(func(string) error {
conn.SetReadDeadline(time.Now().Add(pongWait))
return nil
})
ticker := time.NewTicker(pingPeriod)
defer ticker.Stop()
for range ticker.C {
if err := conn.WriteControl(
websocket.PingMessage, nil, time.Now().Add(10*time.Second),
); err != nil {
return
}
}
}
nhooyr.io/websocket: alternativa con context nativo
nhooyr.io/websocket simplifica el API y trata la cancelación como ciudadano de primera clase:
import "nhooyr.io/websocket"
http.HandleFunc("/ws", func(w http.ResponseWriter, r *http.Request) {
conn, err := websocket.Accept(w, r, &websocket.AcceptOptions{
OriginPatterns: []string{"localhost:*"},
})
if err != nil {
return
}
defer conn.CloseNow()
ctx := r.Context()
for {
typ, data, err := conn.Read(ctx)
if err != nil {
break
}
if err := conn.Write(ctx, typ, data); err != nil {
break
}
}
})
Antipatrón: escrituras concurrentes sin sincronización
El error más frecuente con gorilla/websocket es llamar a WriteMessage desde varias goroutines al mismo tiempo. La documentación es explícita: solo una goroutine puede escribir a la vez. La solución es usar un mutex o, mejor, canalizar todos los envíos por una goroutine dedicada:
// MAL: dos goroutines escriben concurrentemente
go func() { conn.WriteMessage(websocket.TextMessage, msg1) }()
go func() { conn.WriteMessage(websocket.TextMessage, msg2) }()
// BIEN: canal de salida + goroutine escritora única
send := make(chan []byte, 64)
go func() {
for msg := range send {
conn.WriteMessage(websocket.TextMessage, msg)
}
}()
send <- msg1
send <- msg2
