You wrote a pathfinding algorithm in Go that crunches numbers fast. You want to show it off on a website so anyone can click a button and see the result instantly. You don't want to set up a server. You want the code to run right inside the user's browser tab. Go can do this. It compiles to WebAssembly, a binary format that browsers understand. The result feels like native code speed but runs in a sandbox.
The browser is not an OS
Go expects an operating system. It expects file descriptors, a process scheduler, and system calls. The browser is not an OS. It is a document object model with an event loop and a JavaScript runtime. Go bridges this gap with a fake OS layer.
When you build for the browser, you set two environment variables. GOOS=js tells the compiler to target the JavaScript environment. GOARCH=wasm tells it to emit WebAssembly instructions. The compiler generates a .wasm binary that contains your code plus a small runtime. This runtime implements the standard library by talking to JavaScript.
The missing piece is the glue code. Go ships a file called wasm_exec.js. This file lives in your Go installation at $GOROOT/misc/wasm/wasm_exec.js. It sets up the bridge. It creates a fake file system, redirects standard output to the browser console, and polls the Go scheduler so the browser stays responsive. You must copy this file to your web server. If you don't, the browser throws a 404 error and nothing happens.
Copy the version of wasm_exec.js that matches your Go compiler. The file changes between versions. A mismatch causes a runtime panic with a version error. The browser console shows wasm_exec.js version mismatch if the versions differ.
WASM is the engine. wasm_exec.js is the steering wheel.
Minimal example
Here's the simplest Go program that runs in the browser. It prints a message to the console and then waits forever.
package main
import (
"syscall/js"
)
// main prints to console and blocks forever.
func main() {
// Access the global console object from JavaScript.
console := js.Global().Get("console")
// Call the log method with a string argument.
console.Call("log", "Go is running!")
// Block the main goroutine to keep the WASM instance alive.
// The program exits immediately if main returns.
select {}
}
The HTML file loads the glue script and fetches the compiled WASM binary.
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Go WASM</title>
</head>
<body>
<!-- Load the glue code that bridges Go and the browser. -->
<script src="wasm_exec.js"></script>
<script>
// Instantiate the Go runtime and fetch the WASM module.
const go = new Go();
WebAssembly.instantiateStreaming(fetch("main.wasm"), go.importObject)
.then((result) => go.run(result.instance));
</script>
</body>
</html>
Compile the Go code to a WASM binary targeting the JavaScript environment.
# GOOS=js targets the JavaScript runtime. GOARCH=wasm selects WebAssembly.
GOOS=js GOARCH=wasm go build -o main.wasm main.go
Serve the files with any static server. Open the HTML file in the browser. Check the developer console. You should see Go is running!.
Block main. The browser doesn't restart your program.
How the glue works
When the browser loads the HTML, it executes the script. The script creates a Go object from wasm_exec.js. This object prepares the environment. It sets up the fake file system, the fake standard library, and the event loop.
Then WebAssembly.instantiateStreaming fetches main.wasm. The browser downloads the binary and compiles it to machine code. The go.run call starts the Go program. Your main function executes. It calls into JavaScript to log a message. Then it hits select {} and blocks. The goroutine scheduler keeps running in the background. The program stays alive.
Go's standard library works mostly as expected. fmt.Println prints to the browser console. wasm_exec.js redirects standard output automatically. net/http sends requests using the browser's fetch API under the hood. You can call external APIs. However, you cannot open raw TCP sockets. The browser sandbox blocks that. If you need a WebSocket, use a library that wraps the browser's WebSocket API, or rely on net/http for standard requests.
The browser runs on an event loop. Go runs on a scheduler with goroutines. wasm_exec.js connects the two. It sets up a polling loop that checks if Go has work to do. When Go blocks on I/O or a channel, the poller yields back to the browser. This keeps the UI responsive. If you do a tight loop in Go without yielding, you freeze the browser tab. Always yield in long computations.
Call a JavaScript function from Go to force a yield. Or use time.Sleep with a small duration. The scheduler will pause the goroutine and let the browser process events. A frozen tab is the most common complaint from users.
Realistic example: Exporting functions
Real apps do more than print to console. They respond to user input. You can export Go functions to JavaScript. Use js.FuncOf to create a bridge. This function wraps a Go function so JavaScript can call it.
This example exports a Go function so JavaScript can call it.
package main
import (
"syscall/js"
)
// calculate adds two numbers passed from JavaScript.
func calculate(this js.Value, args []js.Value) interface{} {
// Extract the first argument as a float64.
a := args[0].Float()
// Extract the second argument as a float64.
b := args[1].Float()
// Return the sum back to JavaScript.
return a + b
}
// main registers the function and blocks.
func main() {
// Create a JS function wrapper around the Go function.
fn := js.FuncOf(calculate)
// Release the JS function wrapper when done to prevent memory leaks.
// In a long-running WASM app, forgetting this leaks memory in the JS heap.
defer fn.Release()
// Attach the function to the global window object.
js.Global().Set("calculate", fn)
// Keep the program running.
select {}
}
JavaScript can now call window.calculate(1, 2) and get 3. The js.FuncOf wrapper handles the type conversion. JavaScript passes arguments as js.Value objects. You extract the data using methods like Float(), String(), or Int(). JavaScript numbers are all floating point. Go converts them to float64 automatically. If you need an integer, call Int() on the value.
The defer fn.Release() call is critical. js.FuncOf allocates memory in the JavaScript heap. It also pins the Go function so the Go garbage collector cannot reclaim it. If you create functions and never release them, memory grows until the tab crashes. Always release the wrapper when you are done.
Export functions sparingly. Every bridge call crosses the Go-JS boundary and costs time.
Pitfalls and errors
If you forget to import syscall/js, the compiler rejects the code with an undefined package error. The error message says undefined: js. Add the import to fix it.
If you copy the wrong wasm_exec.js, you get a runtime panic about version mismatch. The error looks like wasm_exec.js version mismatch. Always copy the file from your current Go installation.
If your main function returns, the program exits. The WASM instance stops. You lose all state. Always block in main or use a channel to wait for work. A common pattern is select {}. Another pattern is to wait on a channel that never receives.
js.Value holds a reference to JavaScript memory. If you store js.Value objects in Go structs, you can leak memory. The JavaScript garbage collector cannot free the underlying object while Go holds the reference. Use js.ValueOf carefully. Convert to Go types as soon as possible. Drop the js.Value reference.
Leak a js.Value and the tab crashes. Release references when done.
Use build tags to isolate WASM-specific code. Add //go:build js,wasm at the top of a file. The compiler includes this file only when building for the browser. This keeps your code clean and prevents accidental imports of OS-specific packages like os/exec which don't work in WASM.
The compiler rejects imports that don't exist in the WASM build. You get an error like imported and not used or a missing symbol error if you try to use a package that relies on cgo or system calls. WASM builds cannot use cgo. Keep your dependencies pure Go.
Decision matrix
Use Go WASM when you need heavy computation in the browser without a backend. Use Go WASM when you want to share logic between a Go server and a Go frontend. Use plain JavaScript when you are building a simple UI with no complex logic. Use a Go backend with an API when you need to access secrets or database connections. Use a Web Worker when you want to run WASM off the main thread to keep the UI responsive.
WASM adds overhead. Don't use it for a button click handler.