How to Call Go Functions from JavaScript Wasm

Web
Export Go functions with //export, compile to wasm, and call them via WebAssembly.instantiate in JavaScript.

The bridge between Go and the browser

You are building a data visualization tool. The frontend runs in the browser, but the heavy lifting involves parsing millions of log lines or running a custom compression algorithm. JavaScript handles the DOM and user input, but it chokes on the math. You want to write the performance-critical part in Go, compile it to a single binary, and hand it to the browser. The browser does not run Go binaries. It runs WebAssembly. The gap between your Go function and the JavaScript runtime is exactly one compiler directive and one environment variable.

How the export mechanism works

WebAssembly modules are isolated. They cannot see JavaScript variables, and JavaScript cannot see Go variables. They share a single block of linear memory, but they speak different languages. The //export directive tells the Go compiler to carve a specific entry point into the WebAssembly binary. When the browser loads the module, it scans the export table, finds the name you specified, and wires it to a JavaScript function call.

Think of it like a secure drop box. JavaScript places arguments into a shared memory slot and knocks on the door. The Go runtime wakes up, reads the arguments, runs your function, writes the result back into the shared slot, and signals completion. The browser never touches your Go source code. It only talks to the compiled bytecode and the memory buffer.

The Go compiler handles the heavy lifting. It translates your function signature into the WebAssembly calling convention. It marshals Go types into raw bytes, passes them across the boundary, and unmarshals them back on the JavaScript side. You do not need to write C interop code or manage memory manually. The runtime does it automatically.

Run gofmt on your wasm files exactly like you do for server code. The formatting rules do not change because the target platform changed. Let the tool decide indentation and spacing so your team never argues about style.

Exported functions are the public API of your module. Keep them focused and predictable.

A minimal working example

Here is the simplest possible Go file that exposes a function to the browser.

package main

import "syscall/js"

// Add takes two integers and returns their sum.
//export Add
func Add(a, b int) int {
    // The runtime copies these values from JS memory into Go stack space.
    return a + b
}

func main() {
    // Block forever to keep the WASM runtime alive for external calls.
    select {}
}

Compile it with the correct target environment. The GOOS and GOARCH flags tell the compiler to generate WebAssembly bytecode instead of a native executable.

# Target the browser environment and output a raw WASM binary.
GOOS=js GOARCH=wasm go build -o main.wasm main.go

The output is a .wasm file. Serve it alongside a JavaScript file that loads and calls it.

// Stream the binary and compile it simultaneously to avoid double parsing.
const wasmModule = await WebAssembly.instantiateStreaming(
  fetch('main.wasm')
);

// Call the exported function directly from the instance exports.
const result = wasmModule.instance.exports.Add(2, 3);
console.log(result); // prints: 5

The instantiateStreaming API is the standard way to load WebAssembly in modern browsers. It downloads the binary and compiles it simultaneously, avoiding the double-parsing penalty of older APIs. Once the module is ready, instance.exports holds every function you marked with //export. You call them exactly like regular JavaScript functions.

Keep your exported signatures simple. The browser does not care about Go interfaces or method receivers.

What happens under the hood

When you run go build, the compiler strips out everything that does not work in a browser sandbox. You lose access to raw sockets, file systems, and native threads. What remains is a single-threaded runtime that shares memory with the page. The //export directive registers your function in the WebAssembly export section. The compiler generates a thin wrapper that converts JavaScript numbers into Go integers, calls your function, and converts the return value back.

The select {} in main is mandatory. Go programs exit when main returns. If you let main finish, the entire WebAssembly module tears down, and your exported functions vanish. The empty select blocks forever, keeping the runtime alive without consuming CPU cycles. It is the standard idiom for long-lived WebAssembly modules.

Memory works differently than you might expect. Go uses its own garbage collector inside the WebAssembly module. JavaScript uses its own. They do not share pointers. When you pass a number, the runtime copies it. When you pass a string or slice, the runtime allocates a temporary buffer in linear memory, copies the bytes, passes a pointer and length to Go, and cleans up after the call returns. You do not manage this buffer. The Go wasm runtime handles allocation and deallocation automatically.

Use _ when the runtime returns a value you intentionally ignore. It signals to future readers that you considered the return value and made a conscious choice to drop it.

The runtime bridges two garbage collectors. Trust the boundary.

Realistic usage: processing strings and returning results

Real applications rarely just add numbers. They process text, parse JSON, or run algorithms. Here is how you handle strings and return structured data.

package main

import (
    "encoding/json"
    "strings"
    "syscall/js"
)

// ProcessText takes a raw string, reverses it, and returns a JSON object.
//export ProcessText
func ProcessText(input string) string {
    // Reverse the string using the standard library.
    reversed := strings.Reverse(input)
    
    // Marshal the result into JSON so JavaScript can parse it easily.
    result := map[string]string{
        "original": input,
        "reversed": reversed,
    }
    
    // Ignore the error because json.Marshal only fails on unsupported types.
    data, _ := json.Marshal(result)
    return string(data)
}

func main() {
    select {}
}

The Go wasm runtime automatically converts JavaScript strings to Go strings and back. You do not need to pass pointers or lengths manually. The runtime handles the conversion at the boundary. Returning a JSON string is the standard pattern for complex data because JavaScript natively understands JSON, and Go's encoding/json package works perfectly in the wasm environment.

On the JavaScript side, you call it like this:

// Load the module and grab the exported function.
const wasmModule = await WebAssembly.instantiateStreaming(
  fetch('main.wasm')
);

// Pass a string and parse the JSON response.
const response = wasmModule.instance.exports.ProcessText("hello");
const parsed = JSON.parse(response);
console.log(parsed.reversed); // prints: olleh

This pattern scales to any computation. You pass raw data in, run the algorithm in Go, and return a serializable format. The browser handles the UI, Go handles the math.

Write verbose error checks even in wasm code. The if err != nil pattern makes the unhappy path visible and prevents silent data corruption across the memory boundary.

Complex data crosses the boundary as bytes. Serialize early and deserialize late.

Common pitfalls and compiler traps

The Go compiler enforces strict rules for WebAssembly exports. If you break them, you get immediate feedback.

You cannot export unexported functions. If you write //export add for a lowercase function, the compiler rejects it with //export directive requires a declaration of a function named Add. Go requires exported functions to start with a capital letter, and the //export name must match exactly. Public names start with a capital letter. Private names start lowercase. There are no keywords like public or private in Go. The casing is the only rule.

You cannot use unsupported standard library packages. WebAssembly runs in a sandbox. If you import net/http and try to start a server, the compiler complains with net/http: HTTP transport not supported on js/wasm. The wasm environment only supports a subset of the standard library focused on computation, JSON, and basic I/O. Network requests must go through JavaScript's fetch API via syscall/js.

Forgetting the select {} block is the most common runtime bug. The module compiles fine, loads in the browser, and then immediately exits. Your exported functions disappear before JavaScript can call them. The browser console shows no error because the module simply stopped running. Always keep main alive.

Memory leaks happen when you pass large buffers back and forth without letting the garbage collector run. Go's GC runs inside the WebAssembly module, but it only triggers when Go allocates memory. If you spend all your time in JavaScript, the Go GC never wakes up. The runtime provides a js.Global().Call("setTimeout", ...) bridge, but the simpler fix is to batch your work. Process data in chunks, return results, and let the browser yield control between batches.

Do not pass a *string across the boundary. Strings are already cheap to pass by value in Go, and the wasm runtime expects value types for automatic conversion. Pointers break the automatic marshaling layer.

The compiler catches boundary violations at build time. Let it fail fast.

When to use WebAssembly exports

Use //export when you need a direct, synchronous function call from JavaScript that runs compiled Go code. Use syscall/js callbacks when you need Go to trigger JavaScript events, handle DOM updates, or respond to user input. Use a separate WebAssembly module compiled with TinyGo when you need a smaller binary footprint for mobile browsers or constrained environments. Stick to plain JavaScript when the computation is light enough that the overhead of WASM instantiation and type conversion outweighs the performance gain.

Accept interfaces, return structs. Keep your exported functions pure and predictable.

Where to go next