This post provides a step-by-step breakdown of how to run Go tooling inside a browser by compiling it to WASM. The Go logic is exposed to JavaScript using Bazel Go rules, though the process can be replicated without the Bazel build system. The example used is the assembler for the Mrav CPU.
Table of contents
Open Table of contents
Running Go Tools in a Browser
A key advantage of the Mrav CPU monorepo is the portability of its software tooling. Beyond typical development targets like Linux and Mac, Mrav aims to be fully functional within a browser. This capability is highly beneficial for educational purposes and allows users to easily experiment with the project before committing to cloning and setting it up locally.
WASM (WebAssembly) is the core technology leveraged to execute Go code directly within the browser environment.
Writing the binary
The Go logic is made available to the browser through the syscall/js
package. The binary’s structure involves calling relevant APIs to expose the logic, then blocking on a channel read (a read that will never complete, effectively keeping the logic “alive”). The main
function below exposes the Mrav assembler’s logic to the browser.
func main() {
c := make(chan struct{})
js.Global().Set("assembleModule", js.FuncOf(assembleModule))
<-c
}
assembleModule
is a Go function that conforms to the format that the js
package expects. Below is the excerpt:
func assembleModule(this js.Value, args []js.Value) interface{} {
src := args[0].String()
logger := slog.Default()
logger.Info("Got the source, moving on to assembling")
program, err := asm.AssembleModules([]string{src})
if err != nil {
return wrapError(fmt.Errorf("unable to assemble: %w", err))
}
...
My understanding is that this
refers to a context object, though the rest of the function does not utilize it. While the exact details are not fully clear, this structure is expected by the js
package. args
is self-explanatory.
wrapError
returns something like this:
func wrapError(err error) js.Value {
if err == nil {
return js.Null()
}
errWrapper := ErrorWrapper{
Error: err.Error(),
Details: fmt.Sprintf("%T", err), // Include the error type
}
return js.ValueOf(map[string]interface{}{
"error": errWrapper.Error,
"details": errWrapper.Details,
})
}
Once all the logic is exposed, the binary is ready to build!
Building the binary
Bazel is used for building the binaries, but it’s also possible to build the WASM binary using standard Go tooling.
load("@rules_go//go:def.bzl", "go_binary", "go_cross_binary")
package(
default_visibility = [
"//visibility:public",
],
)
go_binary(
name = "assembler",
srcs = [
"assembler.go",
],
cgo = False,
pure = "on",
deps = [
"//software/asm",
"//software/format",
],
)
go_cross_binary(
name = "assembler_wasm",
platform = "//platforms:wasm_js",
target = ":assembler",
)
With standard Go tooling, this primarily involves setting the appropriate GOARCH
and GOOS
environment variables.
Using the binary in the browser
To run the Go binary using WASM in your browser, you first need wasm_exec.js
from the Go project source code. The newest snapshot I currently see (Aug 2025) is here. Make sure you’re looking at the right version for what you’re trying to accomplish (if you don’t care, just make sure you use the latest version).
Next, add the following to your HTML (I added it within the <head>
tag):
<script src="wasm_exec.js"></script>
<script>
const go = new Go();
WebAssembly.instantiateStreaming(
fetch("assembler.wasm"),
go.importObject
).then(result => {
go.run(result.instance);
});
</script>
From this point, the WASM Go binary is executed, and its exported functions become available for your JavaScript to consume!
Calling the logic
Finally, the Mrav CPU assembler logic is ready to be invoked from JavaScript:
let assembled = assembleModule(this.assemblyCode);
if (assembled.error != null) {
this.result = "Processing assembly code:\n" + assembled.error;
return;
}
this.result = "Processing assembly code:\n" + assembled.data;
Conclusion
This brief article highlights Go’s advantages in cross-compilation. It demonstrates how easily one can change the target platform, enabling a Go application to be seamlessly embedded within a browser.
Furthermore, concerning the Mrav CPU, this approach shows how the assembler can be portably integrated into a browser environment. This eliminates the need for end-users to spend time setting up tooling on their machines, as the assembler becomes instantly available once the page loads.
I hope this exploration was useful.
Please consider following on Twitter/X and LinkedIn to stay updated.