Running Go in the Browser with WASM and Web Workers
We’ve recently made big changes to how we execute Go in the browser on boot.dev and want to explain the enhancements. Web Workers are the reason we’ve been able to solve some of the serious browser-related coding problems that were holding us back. Consider this article a sequel to Running Go in the Browser with Web Assembly.
While publishing our latest course, Data Structures and Algorithms, we needed a way to print console output while code is still executing. We ran into a problem when running computationally expensive algorithms in the browser; the browser gets so bogged down that it can’t render new lines of output. We decided to implement web workers, and they solved the problem handily.
The Problem
In the old boot.dev, console output was all printed at once. The program executed, then the output was displayed. We found this to be less than ideal because it is often useful to see when something prints, especially when trying to optimize an algorithm for speed.
For example, this code used to print all of its output at once:
package main
import (
"fmt"
)
func main(){
const max = 100000000
for i := 0; i < max; i++{
if i % (max/10) == 0{
fmt.Println(i)
}
}
}
Since adding Web Workers, now it appropriately prints each number at the time of execution. You can see for yourself on the playground here.
What Is a Web Worker?
Web Workers are a simple means for web content to run scripts in background threads.
In other words, its a way for us to finally break free from the single-threaded clutches of JavaScript! We can offload expensive tasks to another thread of execution. Doing this leaves the browser free to render updates on the screen.
How It Works - The Worker
As you know, we compile code in the editor to WASM on our servers. If you are curious about that part, you can read about it in our previous post. Once the code is compiled to Web Assembly, it’s shipped back to our front end for execution.
To run a Web Worker, we need a script that defines the worker. It’s just a JavaScript file:
addEventListener(
"message",
async (e) => {
// initialize the Go WASM glue
const go = new self.Go();
// e.data contains the code from the main thread
const result = await WebAssembly.instantiate(e.data, go.importObject);
// hijack the console.log function to capture stdout
let oldLog = console.log;
// send each line of output to the main thread
console.log = (line) => {
postMessage({
message: line,
});
};
// run the code
await go.run(result.instance);
console.log = oldLog;
// tell the main thread we are done
postMessage({
done: true,
});
},
false,
);
The worker communicates with the main thread by listening to message events, and sending data back via the postMessage function.
Note: I omitted the wasm_exec.js file that is necessary for the worker to be able to run Go code, but it can be found on your machine if you have Go installed.
cat $(go env GOROOT)/misc/wasm/wasm_exec.js
How it Works - Main Thread
Now that we have a worker file that can execute compiled Web Assembly, let’s take a look at how the main thread communicates with the worker. I built an ES6 module that exports some helper functions:
export function getWorker(lang) {
return {
webWorker: new window.Worker(`/${lang}_worker.js`),
lang,
};
}
export function useWorker(worker, params, callback) {
const promise = new Promise((resolve, reject) => {
worker.webWorker.onmessage = (event) => {
if (event.data.done) {
resolve();
return;
}
if (event.data.error) {
reject(event.data.error);
return;
}
callback(event.data.message);
};
});
worker.webWorker.postMessage(params);
return promise;
}
export function terminateWorker(worker) {
worker.webWorker.terminate();
}
When the page loads we will create a new Web Worker using getWorker. When the user runs some code we send the code to the worker using useWorker. When we navigate away from the code editor we can clean up the worker using terminateWorker.
The useWorker function is the post interesting part. It takes the worker that was created with getWorker, an object called params that will be passed to the worker (it contains the compiled WASM), and a callback function to execute when the worker is finished with the job.
For example, in our Vue app we use these functions as follows:
this.output = [];
this.isLoading = true;
const wasm = await compileGo(this.code);
await useWorker(this.worker, wasm, (data) => {
this.output.push(data);
});
this.isLoading = false;
And because this.output is a reactive property on our Vue instance, each time we receive data from the Web Worker, new output is printed to the console.
Related Articles
How to Make Pure Functions in Golang
Sep 07, 2020 by Lane Wagner - Boot.dev co-founder and backend engineer
Pure functions are often hyped up in the JavaScript world, probably because of the abundance of stateful front end applications. While pure functions have their downsides (i.e. inconvenience, potentially large argument lists), they should be used as much as reasonably possible.
Create a Golang Video Streaming Server Using HLS
Sep 04, 2020 by Lane Wagner - Boot.dev co-founder and backend engineer
In this tutorial, we’ll go step-by-step through building a video streaming API (which will work for music as well) in Go. Don’t worry, it’s surprisingly easy to build a robust media streaming server, especially if we utilize a modern communication protocol, HLS.
Should You Return Empty or Nil Slices in Go?
Sep 03, 2020 by Lane Wagner - Boot.dev co-founder and backend engineer
In Go, we often need to return zero values. Idiomatic Go encourages the use of guard clauses, and guard clauses necessitate the need to return early. When returning early with an error, by convention all other return values should be zero values. The confusion arises with data types like maps and slices. Should maps and slices be returned as a simple nil value, or should an empty but instantiated value be returned?
Top 15 Golang Interview Questions [Updated 2026]
Aug 31, 2020 by Lane Wagner - Boot.dev co-founder and backend engineer
Let’s take a look at some good technical questions to be familiar with, whether you are looking to nail your next Golang interview, or if you’re the interviewer yourself..