What Is WebAssembly and How Does Go Support It

Web
WebAssembly is a binary format for running Go in browsers, compiled using GOOS=wasm and GOARCH=wasm with wasmexport/wasmimport directives.

When Go meets the browser

You built a Go program that parses complex data or runs heavy calculations. It runs fast on your machine. Now you need that same logic inside a webpage so users can interact with the results without waiting for a server round trip. JavaScript is the default language of the browser, but you prefer Go's strict typing and predictable performance. WebAssembly bridges that gap. It lets you compile Go source code into a compact binary format that modern browsers understand. The compiled code runs alongside JavaScript, sharing memory and executing at near-native speed.

What WebAssembly actually is

WebAssembly, or Wasm, is not a programming language. It is a binary instruction format designed for a stack-based virtual machine. Think of it as a universal power adapter. JavaScript is the wall socket built into the browser. Go code is a device with a completely different plug shape. You cannot plug Go directly into the browser runtime. WebAssembly is the adapter that converts your compiled Go program into a shape the browser can accept and execute safely.

The browser loads the Wasm binary into a sandboxed environment, just like it handles JavaScript. The difference is execution speed. JavaScript engines interpret or just-in-time compile code on the fly. Wasm binaries are already compiled into a predictable, low-level format. The browser skips the heavy interpretation step and runs the instructions directly. Go supports this by shipping a dedicated toolchain. The compiler translates your Go source into a .wasm file containing the machine instructions, plus a helper JavaScript file that bootstraps the Go runtime inside the page.

WebAssembly is an adapter, not a replacement for JavaScript.

Minimal example: exporting a function

Here is the smallest Go program that exposes a function to JavaScript. The code uses the syscall/js package to interact with the browser environment.

package main

import (
    "syscall/js"
)

// init runs when the Wasm module loads and registers Go functions with JavaScript.
func init() {
    // Attach the add function to the global JS object so the browser can call it.
    js.Global().Set("add", js.FuncOf(add))
}

// add receives JS values, converts them to Go ints, computes, and returns a JS value.
func add(this js.Value, args []js.Value) interface{} {
    // Convert JS arguments to Go integers.
    a := args[0].Int()
    b := args[1].Int()
    // Return the sum wrapped as a JS value.
    return js.ValueOf(a + b)
}

// main blocks forever so the Wasm instance stays alive.
func main() {
    // Block indefinitely to keep the program running.
    select {}
}

To compile this program, set the target environment to JavaScript OS and WebAssembly architecture. The Go toolchain handles the rest.

# Set target to JavaScript OS and WebAssembly architecture.
GOOS=js GOARCH=wasm go build -o main.wasm main.go

The init function is your handshake with the browser.

How the build and runtime work

When you run the build command, the Go compiler generates two files. The first is main.wasm, which contains the compiled Go code. The second is wasm_exec.js, a helper script that sets up the Go runtime in the browser. You must include wasm_exec.js in your HTML before loading the Wasm module. Without it, the Wasm binary has no way to interact with the browser APIs.

The browser loads the Wasm file and instantiates a module. The init function runs automatically during instantiation. It attaches the add function to the global JavaScript object. Now JavaScript can call add(2, 3) and get 5. The main function uses select {} to block. If main returns, the Wasm instance shuts down. Blocking keeps the program alive so JavaScript can continue calling exported functions.

The wasm_exec.js file does heavy lifting behind the scenes. It polyfills the Go runtime, sets up a linear memory buffer, and implements a cooperative scheduler for goroutines. Because browsers restrict WebAssembly to a single thread by default, the Go scheduler cannot use OS-level threads. Instead, it uses JavaScript's event loop to yield control back to the browser when a goroutine blocks on I/O or waits on a channel. This keeps the UI responsive while your Go code runs.

Go also supports WebAssembly System Interface (WASI). WASI allows Wasm modules to run outside the browser with access to the file system and network. Use GOOS=wasip1 GOARCH=wasm to compile for WASI. This lets you run Go programs in serverless environments or secure sandboxes. The code structure is similar, but you do not use syscall/js. Instead, you use standard Go libraries like os and net.

Block main in Wasm. Let the event loop drive execution.

Realistic example: calling back to JavaScript

Real applications often need to interact with the DOM or call back to JavaScript. Go can import JavaScript functions and invoke them. This allows Go to update the UI or report progress. The syscall/js package provides the bridge. The community accepts the verbose boilerplate because it makes the boundary between Go and JavaScript explicit.

Here is a function that accepts a JavaScript callback and invokes it with a result.

package main

import (
    "syscall/js"
)

// init registers the Go function with JavaScript.
func init() {
    // Expose compute to JS so it can pass a callback.
    js.Global().Set("compute", js.FuncOf(compute))
}

// compute performs work and calls a JavaScript callback with the result.
func compute(this js.Value, args []js.Value) interface{} {
    // Retrieve the callback function passed from JavaScript.
    callback := args[0]
    // Simulate heavy work.
    result := 42
    // Invoke the callback with the result.
    callback.Invoke(js.ValueOf(result))
    return nil
}

// main keeps the Wasm instance running.
func main() {
    // Block to prevent the Wasm instance from exiting.
    select {}
}

The HTML file must load the helper script and instantiate the module. The wasm_exec.js file creates a Go constructor that handles memory and runtime setup.

<!-- Load the Go Wasm runtime helper. -->
<script src="wasm_exec.js"></script>
<!-- Instantiate the Go module and load the Wasm binary. -->
<script>
    // Create a new Go instance.
    const go = new Go();
    // Fetch the Wasm module and run it.
    WebAssembly.instantiateStreaming(fetch("main.wasm"), go.importObject).then((result) => {
        go.run(result.instance);
    });
</script>

Data crossing the boundary costs time. Copy once, compute often.

Pitfalls and compiler errors

The boundary between Go and JavaScript is expensive. Passing a large slice across the boundary copies the data. If you need to process megabytes of data, copy it once, process in Go, and copy the result back. Do not pass small chunks in a loop. The overhead of marshaling and copying will dominate your runtime.

Goroutines work in Wasm, but they run on a single thread by default. The browser limits concurrency. If you need parallelism, you must use multiple Wasm instances or Web Workers. Spawning goroutines in Wasm does not give you multi-core performance. It gives you cooperative concurrency on one thread.

If you forget to import syscall/js, the compiler rejects the program with undefined: js. If you access args[0] when no arguments are passed, the runtime panics with index out of range. Always check the length of the args slice before accessing elements.

Panic handling requires care. If a Go function exported to JavaScript panics, the Wasm instance crashes unless you recover. Wrap exported functions in a recovery block if you want to catch errors and return them to JavaScript. The js.FuncOf wrapper does not recover panics automatically. You must write a helper that calls recover() and converts the panic value into a safe JavaScript return.

Goroutines in Wasm share a single thread. Plan your concurrency carefully.

When to use WebAssembly

Use WebAssembly when you need to run Go code in a browser and require performance better than JavaScript for computation-heavy tasks. Use WebAssembly when you want to reuse existing Go libraries in a web application without rewriting them in JavaScript. Use plain JavaScript when the task involves DOM manipulation, event handling, or simple logic where the overhead of Wasm is not justified. Use a Go backend with an HTTP API when the computation is too heavy for the client device or requires access to server-side resources. Use WASI when you need to run Go in a secure sandbox outside the browser with access to the file system.

Wasm runs Go in the browser. JavaScript runs the browser. Respect the boundary.

Where to go next