gowasm-bindgen for Go Developers#

Full-stack Go with type-safe JavaScript interop. Write Go, compile to WASM, get TypeScript class APIs automatically.

Why This Exists#

Go WASM functions traditionally required awkward js.Value signatures:

// Old way - awkward js.Value signatures
func myFunc(this js.Value, args []js.Value) interface{} {
    name := args[0].String()
    return js.ValueOf("Hello, " + name)
}

The args parameter is []js.Value (untyped), and the return is interface{} (untyped). TypeScript sees these functions as any.

With the new gowasm-bindgen, you write normal Go functions:

// New way - normal Go functions
func MyFunc(name string) string {
    return "Hello, " + name
}

gowasm-bindgen reads your Go source code, infers types from function signatures, and generates:

  1. TypeScript client with proper types (myFunc(name: string): Promise<string>)
  2. Go WASM bindings that handle the js.Value conversions automatically

The TypeScript class name is derived from the directory name (e.g., wasm/GoWasm class). Override with --class-name.

How It Works#

gowasm-bindgen uses source-based type inference - it parses your Go source file’s AST to extract:

  • Exported function signatures (capitalized names, no receivers)
  • Struct definitions with JSON tags
  • Parameter names and types
  • Return types including (T, error) patterns

No test files, annotations, or runtime analysis required. Just point it at your Go source file.

gowasm-bindgen wasm/main.go

This single command generates bindings, copies the runtime, and compiles WASM.

TinyGo vs Standard Go#

Standard GoTinyGo
Binary size~2.4 MB~200 KB
Gzipped~600 KB~90 KB
Language supportFullPartial
StdlibFullPartial
ReflectionFullLimited

TinyGo Limitations: TinyGo doesn’t support all Go features. Notable gaps include:

  • reflect.Value.Call() and reflect.MakeFunc()
  • Some encoding/json edge cases
  • go:linkname directives
  • Three-index slicing (a[1:2:3])

See the TinyGo Language Support page for details.

When to use Standard Go: If your code uses unsupported features, or you need full reflect capabilities for JSON marshaling complex types, use standard Go and accept the larger binary.

# TinyGo (default) - smaller binaries, some limitations
gowasm-bindgen wasm/main.go

# Standard Go - larger but full compatibility
gowasm-bindgen wasm/main.go --compiler go

Writing Go Functions for WASM#

Basic Functions#

Write normal Go functions with concrete types:

package main

// Greet returns a greeting message
func Greet(name string) string {
    return "Hello, " + name + "!"
}

// Calculate performs arithmetic
func Calculate(a int, b int, op string) int {
    switch op {
    case "add":
        return a + b
    case "sub":
        return a - b
    default:
        return 0
    }
}

Requirements:

  • Functions must be exported (start with uppercase letter)
  • Functions must be package-level (no receivers)
  • Use concrete types (avoid interface{} when possible)

Struct Returns#

Define structs with JSON tags for TypeScript interfaces:

type User struct {
    DisplayName string `json:"displayName"`
    Status      string `json:"status"`
}

func FormatUser(name string, age int, active bool) User {
    status := "inactive"
    if active {
        status = "active"
    }
    return User{
        DisplayName: fmt.Sprintf("%s (%d)", name, age),
        Status:      status,
    }
}

This generates:

interface User {
    displayName: string;
    status: string;
}

class GoWasm {
    formatUser(name: string, age: number, active: boolean): Promise<User>;
}

Error Returns#

Functions that return (T, error) automatically throw in TypeScript:

func Divide(a, b int) (int, error) {
    if b == 0 {
        return 0, errors.New("division by zero")
    }
    return a / b, nil
}

This generates:

class GoWasm {
    // Throws on error, returns number on success
    divide(a: number, b: number): Promise<number>;
}

// Usage:
try {
    const result = await wasm.divide(10, 0);
} catch (e) {
    console.error(e.message);  // "division by zero"
}

Testing (Optional)#

Tests are no longer required for type generation, but you should still write them:

func TestGreet(t *testing.T) {
    got := Greet("World")
    want := "Hello, World!"
    if got != want {
        t.Errorf("Greet() = %v, want %v", got, want)
    }
}

These are normal unit tests - no js.Value required!

Typed Arrays#

Go byte and numeric slices map to TypeScript typed arrays:

// HashData processes binary data efficiently
func HashData(data []byte) []byte {
    hash := make([]byte, 4)
    for i, b := range data {
        hash[i%4] ^= b
    }
    return hash
}

// ProcessNumbers works with 32-bit integers
func ProcessNumbers(nums []int32) []int32 {
    result := make([]int32, len(nums))
    for i, n := range nums {
        result[i] = n * 2
    }
    return result
}

Performance note: Byte arrays ([]byte) use js.CopyBytesToGo() and js.CopyBytesToJS() for efficient bulk copying (~10-100x faster for large arrays). Other numeric types use element-by-element iteration.

Void Callbacks#

Functions can accept callback parameters (void only). Callbacks work in both worker and sync modes:

// ForEach iterates over items and calls the callback for each.
// The callback is invoked synchronously during ForEach execution.
func ForEach(items []string, callback func(string, int)) {
    for i, item := range items {
        callback(item, i)
    }
}

This generates:

class GoWasm {
    // callback parameter type is inferred
    forEach(items: string[], callback: (arg0: string, arg1: number) => void): Promise<void>;
}

// Usage (worker mode - async):
await wasm.forEach(["a", "b", "c"], (item, index) => {
    console.log(`${index}: ${item}`);
});

Worker mode (default): Uses fire-and-forget message passing. Go invokes callbacks by posting messages to the main thread. The UI stays responsive, and callbacks are executed asynchronously.

Sync mode: Callbacks are invoked directly and synchronously.

Limitations:

  • Callbacks must have no return value (void)
  • Callbacks are only valid during the Go function’s execution (do not store for later use)
  • No nested callbacks (callback taking callback)
  • In worker mode, callback errors are logged to console but cannot propagate back to Go
  • In sync mode, if the callback throws, Go will panic (caught by error boundary)

Not supported:

// Callbacks with return values - NOT supported
func Filter(items []string, predicate func(string) bool) []string

Type Mapping#

Go TypeTypeScript Type
stringstring
int, int64number
float32, float64number
boolboolean
[]byte, []uint8Uint8Array
[]int8Int8Array
[]int16, []uint16Int16Array, Uint16Array
[]int32, []uint32Int32Array, Uint32Array
[]float32, []float64Float32Array, Float64Array
[]T (other)T[]
map[string]T{[key: string]: T}
func(T, U) (void)(arg0: T, arg1: U) => void
Unknownany

Generated Files#

When you run gowasm-bindgen, it generates:

  1. TypeScript Client (<class-name>.ts): Type-safe API (e.g., go-wasm.ts for class GoWasm)
  2. Web Worker (worker.js): Loads and runs WASM in background thread
  3. Go Runtime (wasm_exec.js): Copied from your TinyGo/Go installation
  4. WASM Binary (wasm.wasm): Compiled WebAssembly
  5. Go Bindings (bindings_gen.go): Handles js.Value conversions (in source directory)

The Go bindings file registers your functions and handles all the js.Value marshaling:

// bindings_gen.go (generated)
package main

import "syscall/js"

func init() {
    js.Global().Set("greet", js.FuncOf(func(this js.Value, args []js.Value) interface{} {
        // Automatic conversion from js.Value to Go types
        name := args[0].String()
        result := Greet(name)
        // Automatic conversion from Go types to js.Value
        return js.ValueOf(result)
    }))
}

Important: Add bindings_gen.go to your .gitignore - it’s a build artifact.

Workflow#

  1. Write Go functions with normal signatures:

    // wasm/main.go
    func Greet(name string) string { ... }
  2. Build everything with one command:

    gowasm-bindgen wasm/main.go
  3. Use in TypeScript:

    import { GoWasm } from './generated/go-wasm';
    const wasm = await GoWasm.init('./worker.js');
    const result = await wasm.greet('World');

Limitations#

  • Exported functions only: Only package-level exported functions are available
  • Concrete types: interface{} returns become any in TypeScript
  • No function overloads: Go doesn’t support them either
  • Struct field tags: Use JSON tags for TypeScript-friendly field names
  • No runtime validation: Generated types don’t validate at runtime

Generated API#

gowasm-bindgen generates a TypeScript class with a name derived from the directory (e.g., wasm/GoWasm):

// Worker mode (default): generated go-wasm.ts
export class GoWasm {
  static async init(workerUrl: string): Promise<GoWasm>;
  greet(name: string): Promise<string>;
  calculate(a: number, b: number, op: string): Promise<number>;
  terminate(): void;
}

// Sync mode (-m sync): generated go-wasm.ts
export class GoWasm {
  static async init(wasmSource: string | BufferSource): Promise<GoWasm>;
  greet(name: string): string;  // No Promise - synchronous
  calculate(a: number, b: number, op: string): number;
}

The sync mode init() accepts either a URL string (browser) or BufferSource (Node.js).

Your TypeScript users import and use it:

import { GoWasm } from './generated/go-wasm';

const wasm = await GoWasm.init('./worker.js');
const result = await wasm.greet('World');
wasm.terminate();

Project Structure#

your-project/
├── wasm/                 # All Go code
│   ├── main.go           # Your WASM implementation
│   ├── main_test.go      # Optional unit tests
│   └── bindings_gen.go   # Generated Go bindings (gitignored)
├── src/                  # TypeScript source
│   └── app.ts            # TypeScript frontend
├── public/               # Static assets
│   └── index.html
└── generated/            # Generated output (gitignored)
    ├── go-wasm.ts        # Generated TypeScript client
    ├── worker.js         # Generated Web Worker wrapper
    ├── wasm_exec.js      # Go runtime (copied)
    └── wasm.wasm         # Compiled WASM

Complete Example#

See the examples/simple/ directory for a working demo with:

  • 5 WASM functions with different parameter/return types
  • Normal Go functions (no js.Value signatures)
  • Unit tests
  • TinyGo build with size optimizations
  • TypeScript web demo
  • TypeScript verification tests

FAQ#

Do I need to write tests?#

No. Tests are optional. gowasm-bindgen infers types directly from your function signatures:

// No test needed - type inference is automatic
func Greet(name string) string {
    return "Hello, " + name
}
// → TypeScript: greet(name: string): Promise<string>

However, you should still write tests for correctness!

Why source-based instead of annotations?#

  1. Zero boilerplate: No special annotations or tags needed
  2. Type safety: Go compiler enforces correct types
  3. Normal Go code: Write functions like you always do
  4. No learning curve: If you know Go, you know how to use this