Creating and Using WebSocket Connections in Go
Overview
WebSockets provide a persistent, full-duplex connection between a client and server, enabling real-time communication without repeated HTTP handshakes. In Go, the github.com/gorilla/websocket package is the standard way to work with WebSockets. This guide walks through building a working WebSocket server and client, handling multiple connections, and keeping connections alive.
Prerequisites
- Go 1.18+ installed
- Basic familiarity with Go's
net/httppackage - Understanding of what WebSockets are (see WebSockets: A Comprehensive Guide)
Installing the Gorilla WebSocket Package
go get -u github.com/gorilla/websocket
Basic Echo Server
The simplest WebSocket server reads a message and echoes it back.
package main
import (
"log"
"net/http"
"github.com/gorilla/websocket"
)
var upgrader = websocket.Upgrader{
ReadBufferSize: 1024,
WriteBufferSize: 1024,
CheckOrigin: func(r *http.Request) bool {
return true // allow all origins in development
},
}
func handleConnection(w http.ResponseWriter, r *http.Request) {
conn, err := upgrader.Upgrade(w, r, nil)
if err != nil {
log.Println("upgrade error:", err)
return
}
defer conn.Close()
for {
msgType, msg, err := conn.ReadMessage()
if err != nil {
log.Println("read error:", err)
break
}
log.Printf("received: %s", msg)
if err = conn.WriteMessage(msgType, msg); err != nil {
log.Println("write error:", err)
break
}
}
}
func main() {
http.HandleFunc("/ws", handleConnection)
log.Println("Server listening on :8080")
log.Fatal(http.ListenAndServe(":8080", nil))
}
websocket.Upgrader performs the HTTP → WebSocket handshake. In production, CheckOrigin should validate the Origin header against an allowlist rather than returning true unconditionally.
Basic Client
package main
import (
"log"
"github.com/gorilla/websocket"
)
func main() {
conn, _, err := websocket.DefaultDialer.Dial("ws://localhost:8080/ws", nil)
if err != nil {
log.Fatal("dial error:", err)
}
defer conn.Close()
// Send a message
err = conn.WriteMessage(websocket.TextMessage, []byte("Hello, server!"))
if err != nil {
log.Fatal("write error:", err)
}
// Read the echo
_, msg, err := conn.ReadMessage()
if err != nil {
log.Fatal("read error:", err)
}
log.Printf("received: %s", msg)
}
Broadcasting to Multiple Clients
Real applications often need to send a message to all connected clients. The standard pattern uses a shared hub struct with goroutine-safe access.
package main
import (
"log"
"net/http"
"sync"
"github.com/gorilla/websocket"
)
type Hub struct {
mu sync.RWMutex
clients map[*websocket.Conn]bool
}
func NewHub() *Hub {
return &Hub{clients: make(map[*websocket.Conn]bool)}
}
func (h *Hub) register(conn *websocket.Conn) {
h.mu.Lock()
h.clients[conn] = true
h.mu.Unlock()
}
func (h *Hub) unregister(conn *websocket.Conn) {
h.mu.Lock()
delete(h.clients, conn)
h.mu.Unlock()
}
func (h *Hub) broadcast(msg []byte) {
h.mu.RLock()
defer h.mu.RUnlock()
for conn := range h.clients {
if err := conn.WriteMessage(websocket.TextMessage, msg); err != nil {
log.Println("broadcast write error:", err)
}
}
}
var hub = NewHub()
var upgrader = websocket.Upgrader{
CheckOrigin: func(r *http.Request) bool { return true },
}
func handleConn(w http.ResponseWriter, r *http.Request) {
conn, err := upgrader.Upgrade(w, r, nil)
if err != nil {
return
}
hub.register(conn)
defer func() {
hub.unregister(conn)
conn.Close()
}()
for {
_, msg, err := conn.ReadMessage()
if err != nil {
break
}
hub.broadcast(msg)
}
}
func main() {
http.HandleFunc("/ws", handleConn)
log.Fatal(http.ListenAndServe(":8080", nil))
}
Ping/Pong Keepalive
TCP connections can silently drop (proxies, NAT timeouts, network changes). The WebSocket protocol includes ping/pong frames to detect broken connections.
import "time"
const (
pongWait = 60 * time.Second
pingPeriod = (pongWait * 9) / 10
writeWait = 10 * time.Second
)
func handleWithKeepalive(w http.ResponseWriter, r *http.Request) {
conn, err := upgrader.Upgrade(w, r, nil)
if err != nil {
return
}
defer conn.Close()
conn.SetReadDeadline(time.Now().Add(pongWait))
conn.SetPongHandler(func(string) error {
conn.SetReadDeadline(time.Now().Add(pongWait))
return nil
})
// Send pings in a separate goroutine
go func() {
ticker := time.NewTicker(pingPeriod)
defer ticker.Stop()
for range ticker.C {
conn.SetWriteDeadline(time.Now().Add(writeWait))
if err := conn.WriteMessage(websocket.PingMessage, nil); err != nil {
return
}
}
}()
for {
_, msg, err := conn.ReadMessage()
if err != nil {
break
}
log.Printf("received: %s", msg)
}
}
If the client does not respond to a ping within pongWait, ReadMessage returns an error and the connection is closed.
Origin Checking in Production
Never use CheckOrigin: func(r *http.Request) bool { return true } in production — it allows cross-site WebSocket hijacking.
var allowedOrigins = map[string]bool{
"https://yourdomain.com": true,
"https://www.yourdomain.com": true,
}
var upgrader = websocket.Upgrader{
CheckOrigin: func(r *http.Request) bool {
origin := r.Header.Get("Origin")
return allowedOrigins[origin]
},
}
Error Handling Reference
| Error condition | Gorilla behavior | What to do |
|---|---|---|
| Client disconnects cleanly | ReadMessage returns CloseError | Close connection, clean up |
| Network timeout | Returns net.Error with Timeout() true | Log, close, let client reconnect |
| Ping timeout (no pong) | ReadMessage returns i/o timeout | Close and remove from hub |
| Invalid message type | Returns ErrCloseSent | Log and break read loop |
