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:
- TypeScript client with proper types (
myFunc(name: string): Promise<string>) - Go WASM bindings that handle the
js.Valueconversions 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.goThis single command generates bindings, copies the runtime, and compiles WASM.
TinyGo vs Standard Go#
| Standard Go | TinyGo | |
|---|---|---|
| Binary size | ~2.4 MB | ~200 KB |
| Gzipped | ~600 KB | ~90 KB |
| Language support | Full | Partial |
| Stdlib | Full | Partial |
| Reflection | Full | Limited |
TinyGo Limitations: TinyGo doesn’t support all Go features. Notable gaps include:
reflect.Value.Call()andreflect.MakeFunc()- Some
encoding/jsonedge cases go:linknamedirectives- 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 goWriting 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) []stringType Mapping#
| Go Type | TypeScript Type |
|---|---|
string | string |
int, int64 | number |
float32, float64 | number |
bool | boolean |
[]byte, []uint8 | Uint8Array |
[]int8 | Int8Array |
[]int16, []uint16 | Int16Array, Uint16Array |
[]int32, []uint32 | Int32Array, Uint32Array |
[]float32, []float64 | Float32Array, Float64Array |
[]T (other) | T[] |
map[string]T | {[key: string]: T} |
func(T, U) (void) | (arg0: T, arg1: U) => void |
| Unknown | any |
Generated Files#
When you run gowasm-bindgen, it generates:
- TypeScript Client (
<class-name>.ts): Type-safe API (e.g.,go-wasm.tsfor classGoWasm) - Web Worker (
worker.js): Loads and runs WASM in background thread - Go Runtime (
wasm_exec.js): Copied from your TinyGo/Go installation - WASM Binary (
wasm.wasm): Compiled WebAssembly - Go Bindings (
bindings_gen.go): Handlesjs.Valueconversions (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#
Write Go functions with normal signatures:
// wasm/main.go func Greet(name string) string { ... }Build everything with one command:
gowasm-bindgen wasm/main.goUse 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 becomeanyin 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 WASMComplete Example#
See the examples/simple/ directory for a working demo with:
- 5 WASM functions with different parameter/return types
- Normal Go functions (no
js.Valuesignatures) - 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?#
- Zero boilerplate: No special annotations or tags needed
- Type safety: Go compiler enforces correct types
- Normal Go code: Write functions like you always do
- No learning curve: If you know Go, you know how to use this