Skip to main content

Creating and Using WebSocket Connections in Go

· 4 min read
PSVNL Sai Kumar
Senior Software Development Engineer, Oracle

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

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 conditionGorilla behaviorWhat to do
Client disconnects cleanlyReadMessage returns CloseErrorClose connection, clean up
Network timeoutReturns net.Error with Timeout() trueLog, close, let client reconnect
Ping timeout (no pong)ReadMessage returns i/o timeoutClose and remove from hub
Invalid message typeReturns ErrCloseSentLog and break read loop