Skip to Content
Course content

139: WebSockets in Go with gorilla/websocket

Click on the "Edit" button in the top corner of the screen to edit your slide content.

Think about the difference between sending a series of letters through the mail and having a phone call. With a letter (which is basically how standard HTTP works), you send a request, wait for it to travel, and then wait for the other person to write back and mail a response. If you want updated information, you have to send another letter. It's slow, and the other person can't just shout "Hey, something changed!" unless you've already asked them for an update.

A WebSocket is that phone call. You go through a brief "handshake" to establish the connection, but once that call is active, the line stays open. Either side can speak whenever they want, and the other side hears it instantly. In Go, the gorilla/websocket package is the industry standard for managing this "open line."

Turning an HTTP request into a persistent connection

WebSockets actually start their life as a standard HTTP request. The client asks the server, "Hey, can we upgrade this connection to a WebSocket?" To handle this in Go, we use an Upgrader. I like to think of the Upgrader as the receptionist who verifies your credentials before transferring you to a private, direct line.

var upgrader = websocket.Upgrader{
    ReadBufferSize:  1024,
    WriteBufferSize: 1024,
    CheckOrigin: func(r *http.Request) bool {
        // In production, you'd check the origin for security. 
        // For now, we'll just allow everything.
        return true 
    },
}

func handleConnections(w http.ResponseWriter, r *http.Request) {
    // Upgrade initial GET request to a websocket
    ws, err := upgrader.Upgrade(w, r, nil)
    if err != nil {
        log.Fatal(err)
    }
    defer ws.Close()
    
    // Now 'ws' is our open phone line.
}

Notice that CheckOrigin function. If you don't define it, Gorilla defaults to a strict check that might block your frontend if it's running on a different port (like a React app on 3000 talking to a Go API on 8080). I've set it to return true here just to keep us moving, but be careful with that in a real app.

The "Listen and Speak" loop

Once the connection is upgraded, you're no longer dealing with http.ResponseWriter. You're dealing with a *websocket.Conn. Because this connection is persistent, we usually wrap the logic in a for loop. If the loop breaks (because the user closed their browser or the network dropped), the connection is dead.

Let's look at a real example: a server that monitors system memory and pushes updates to the client every second without the client ever asking for them.

func streamMemoryUsage(ws *websocket.Conn) {
    for {
        // Imagine getMemUsage() is a helper that returns a string
        stats := getMemUsage() 
        
        // WriteMessage takes the message type (TextMessage or BinaryMessage) 
        // and the payload as a byte slice.
        err := ws.WriteMessage(websocket.TextMessage, []byte(stats))
        if err != nil {
            log.Printf("Connection lost: %v", err)
            break 
        }
        
        time.Sleep(1 * time.Second)
    }
}

Managing bidirectional traffic with Goroutines

Here is where a lot of developers trip up. If you have a for loop that is blocking while waiting to ReadMessage from the client, you can't simultaneously WriteMessage to them on the same goroutine. It's like trying to listen to someone and talk over them at the exact same time on a single-track brain.

The fix is simple: use Go's concurrency. I typically spin up a separate goroutine for reading and keep the main handler for writing, or vice versa. This allows the server to push data (like our memory stats) while still listening for "stop" or "configure" commands from the user.

func handleConnections(w http.ResponseWriter, r *http.Request) {
    ws, _ := upgrader.Upgrade(w, r, nil)
    defer ws.Close()

    // Start a goroutine to handle incoming messages from the client
    go func() {
        for {
            _, msg, err := ws.ReadMessage()
            if err != nil {
                break
            }
            log.Printf("Client says: %s", msg)
        }
    }()

    // The main loop handles pushing data TO the client
    for {
        err := ws.WriteMessage(websocket.TextMessage, []byte("Server Heartbeat"))
        if err != nil {
            break
        }
        time.Sleep(5 * time.Second)
    }
}

I'll leave you with one tip: always remember to defer ws.Close(). Leaking WebSocket connections is a fast track to crashing your server because each open connection consumes a file descriptor and some memory. Be diligent about cleaning up.




📋 Practical Task

Build a Live System Log Streamer

Your task is to create a Go server that simulates a "Live Log Streamer." Instead of a user requesting logs via a GET request, the server should push a new "log line" to the client every 500 milliseconds.

  • Implement a /logs endpoint that upgrades the connection to a WebSocket using gorilla/websocket.
  • Create a slice of strings containing fake log messages (e.g., "User logged in", "Database query took 20ms", "Cache miss on key: user_123").
  • Use a for loop and a time.Ticker (or time.Sleep) to pick a random log message from your slice and push it to the client.
  • Ensure that if the client disconnects, the server stops the loop for that specific connection to prevent goroutine leaks.
  • Bonus: Implement a "Ping" mechanism where if the client sends the message "PING", the server responds immediately with "PONG" without interrupting the log stream.
Rating
0 0

There are no comments for now.

to be the first to leave a comment.