/blog

🔌 Websockets With Golang

A simple pattern to get started using websockets with Golang.

Every website I've built recently has made use of websockets. The frontend is always Javascript and the backend is always Go. I've now gotten used to a programming pattern that I've been implementing over and over for doing websockets. There are a lot of ways to implement websockets in the frontend and backend, but here's what I like to use.

Frontend code for websockets

The frontend just consists of a few lines of Javascript. No need for externally libraries. Basically these functions are pulled straight from the Web APIs for Websockets from MDN:

var socket;
const socketMessageListener = (e) => {
    console.log(e.data);
};
const socketOpenListener = (e) => {
    console.log('Connected');
    socket.send(JSON.stringify({ message: "hello, server" }))
};
const socketErrorListener = (e) => {
    console.error(e);
}
const socketCloseListener = (e) => {
    if (socket) {
        console.log('Disconnected.');
    }
    var url = window.origin.replace("http", "ws") + '/ws';
    socket = new WebSocket(url);
    socket.onopen = socketOpenListener;
    socket.onmessage = socketMessageListener;
    socket.onclose = socketCloseListener;
    socket.onerror = socketErrorListener;
};
window.addEventListener('load', (event) => {
    socketCloseListener();
});

The program is started by calling the closing listener. Since websockets naturally uses Keep-Alive this code will automatically re-join broken connections so you don't have to code that yourself! Modern browser's are great eh.

Backend code for websockets

There are basically two libraries for doing this in Go. Both of them are pretty much the same - zero dependencies, fast, they bind JSON, and they wrap the http std library.

There's the old and reliable gorilla/websocket.

var wsupgrader = websocket.Upgrader{
	ReadBufferSize:  1024,
	WriteBufferSize: 1024,
	CheckOrigin: func(r *http.Request) bool {
		return true
	},
}

func handleWebsocket(w http.ResponseWriter, r *http.Request) (err error) {
	c, errUpgrade := wsupgrader.Upgrade(w, r, nil)
	if errUpgrade != nil {
		return errUpgrade
	}
	defer c.Close()

	for {
		var p interface{}
		err := c.ReadJSON(&p)
		if err != nil {
			log.Debug("read:", err)
			break
		}
		log.Debugf("recv: %v", p)
		c.WriteJSON(struct{ Message string }{
			"hello, browser",
		})

	}
	return
}

And there's the more recent nhooyr/websocket, which I've forked into schollz/websocket to remove all the test dependencies:

import (
	"github.com/schollz/websocket"
	"github.com/schollz/websocket/wsjson"
)

func handleWebsocket(w http.ResponseWriter, r *http.Request) (err error) {
	c, err := websocket.Accept(w, r, nil)
	if err != nil {
		return
	}
	defer c.Close(websocket.StatusInternalError, "internal error")

	ctx, cancel := context.WithTimeout(r.Context(), time.Hour*120000)
	defer cancel()

	for {
		var v interface{}
		err = wsjson.Read(ctx, c, &v)
		if err != nil {
			break
		}
		log.Debugf("received: %v", v)
		err = wsjson.Write(ctx, c, struct{ Message string }{
			"hello, browser",
		})
		if err != nil {
			break
		}
	}
	if websocket.CloseStatus(err) == websocket.StatusGoingAway {
		err = nil
	}
	c.Close(websocket.StatusNormalClosure, "")
	return
}

The latter example will eventually handle HTTP/2 and it seems the gorilla/websocket never will (unless they start updating it!).

Try it!

There is code for this on my Github.

January 9, 2020

📷 Photos I took (2019) 📚 Books I read (2019)