· 6 min read ·

The WebAssembly Tooling Layer Go Was Missing

Source: eli-bendersky

Go has had first-class WebAssembly support as a compilation target since Go 1.11, and WASI support landed in Go 1.21 with GOOS=wasip1. You can compile a Go program to a WASM binary with a single command. What Go has lacked, quietly, is the other side of that story: a native way to read, parse, validate, and manipulate WASM files programmatically, in pure Go, without shelling out to external C or Rust tooling.

That gap is what watgo fills.

The Existing Landscape

The WebAssembly ecosystem already has mature tooling, just not in Go. wabt, the WebAssembly Binary Toolkit, has been the reference implementation for years. Written in C++, it includes wat2wasm, wasm2wat, wasm-validate, wasm-objdump, and several other utilities that cover the full lifecycle of working with WASM artifacts. It’s the canonical toolchain when you need to hand-author WAT, validate a binary, or inspect what a compiler emitted.

More recently, the Bytecode Alliance has been building wasm-tools in Rust. It covers similar ground but goes further, with support for the Component Model, WIT interfaces, and a richer programmatic API through its Rust crate. For Rust-based build tooling or host embedders written in Rust, wasm-tools is the obvious choice.

For Go developers, neither option was clean. You could shell out to wat2wasm via exec.Command, but that introduces an implicit system dependency, breaks cross-compilation, and adds friction to any tool distribution story. You could look for Go bindings to wabt via CGo, but CGo has its own set of trade-offs: slower compile times, disabled cross-compilation, and the complexity of managing the C build. A Go developer building a plugin loader, a WASM registry, a build tool, or a testing harness around WASM modules had no good native option.

What watgo Provides

watgo, written by Eli Bendersky, positions itself explicitly as a peer to wabt and wasm-tools: a toolkit with equivalent core capabilities, but in pure, zero-dependency Go. The feature set covers four operations:

  • Parse: WAT text format to an in-memory semantic representation
  • Validate: check the module against the official WebAssembly validation semantics
  • Encode: serialize the in-memory representation to the WASM binary format
  • Decode: read a WASM binary back into the in-memory representation

At the center of all four operations is wasmir, the semantic IR for a WebAssembly module. This is the part worth paying attention to.

wasmir: The Semantic IR

Many binary format libraries stop at a structural representation: they give you the bytes organized into fields and sections, but the meaning is implicit. wasmir is explicitly semantic. It represents a WebAssembly module as Go types that correspond to the actual concepts in the WebAssembly spec: functions, tables, memories, globals, imports, exports, element segments, data segments, and the instruction sequences that make up function bodies.

This distinction matters in practice. If you’re building a tool that needs to reason about a WASM module, a structural representation makes you do extra work to interpret what you’ve read. A semantic representation gives you a model you can query directly. Want to enumerate all exported functions and their signatures? That’s a field access and a range loop over a typed slice, not a hex dump with offsets.

The WebAssembly binary format encodes types using LEB128 variable-length integers, and the module structure is section-based with a fixed ordering. Parsing this correctly, and then mapping it to something that correctly implements the validation rules from the WebAssembly spec, is non-trivial work. watgo handles that translation so library consumers can work at the level of module semantics.

Zero Dependencies Is a Design Choice

The “zero-dependency, pure Go” constraint is worth examining as a deliberate architectural decision rather than a constraint. In Go, zero dependencies means:

  • go install or go get works without any system packages, compilers, or toolchains present beyond the Go toolchain itself
  • Cross-compilation works without modification (GOOS=linux GOARCH=arm64 go build remains trivial)
  • The module graph for anything that imports watgo stays clean
  • There are no CGo build constraints that disable certain go tool features

For a library that’s meant to be embedded in other Go tools, these properties are significant. A WASM build system written in Go that imports watgo can still be distributed as a single static binary. A plugin registry that inspects and validates uploaded WASM artifacts doesn’t pull in system libraries that may or may not be present in the deployment environment.

The trade-off is that you stay within Go’s standard library for everything. No SIMD-accelerated LEB128 decoding, no C library that’s been fuzzed for a decade by the wabt maintainers. For a toolkit at this stage, that’s a reasonable trade.

The CLI

watgo ships a CLI tool that’s designed to be a drop-in replacement for the corresponding wasm-tools commands. You can install it with the standard Go mechanism:

go install github.com/eliben/watgo/cmd/watgo@latest

The core use case is parsing a WAT source file, validating it, and encoding it to binary WASM:

watgo parse stack.wat -o stack.wasm

The interface mirrors wasm-tools closely enough that Bendersky has already migrated his own wasm-wat-samples repository to use watgo instead. For developers who were previously relying on wasm-tools in their build scripts and want a version with no Rust installation requirement, this is a direct substitute for the common cases.

The API

The Go API is where watgo becomes useful for building things. The library exposes watgo and wasmir as separate packages; watgo provides the high-level entry points (parse, validate, encode, decode), and wasmir contains the type definitions for the semantic IR.

A simplified version of programmatic module analysis looks like this:

package main

import (
    "fmt"

    "github.com/eliben/watgo"
    "github.com/eliben/watgo/wasmir"
)

const wasmText = `
(module
  (func (export "add") (param i32 i32) (result i32)
    local.get 0
    local.get 1
    i32.add))
`

func main() {
    mod, err := watgo.ParseWAT([]byte(wasmText))
    if err != nil {
        panic(err)
    }
    for _, exp := range mod.Exports {
        if exp.Desc.Kind == wasmir.FuncExportKind {
            fmt.Printf("exported function: %s\n", exp.Name)
        }
    }
}

The parsed mod is a wasmir.Module, a typed Go struct with fields for every section of a WebAssembly module. The type system does most of the work of making it navigable.

What This Enables

Pure-Go WASM tooling opens up a category of tools that were previously awkward to build. A few concrete examples:

Plugin systems. Go programs that accept WASM plugins (as an alternative to native .so plugins) can validate and inspect submitted modules before executing them, using the same binary that hosts the runtime. No subprocess, no CGo.

WASM registries and proxies. A service that stores and serves WASM artifacts can validate uploads against the WebAssembly spec on ingestion, inspect module exports to index capabilities, and transform modules if needed, all in process.

Build tooling. A Go-based build system that compiles source code to WASM and then post-processes the output, linking modules, stripping sections, or patching imports, now has a native API for the manipulation step.

Testing infrastructure. Integration test harnesses that need to inspect WASM output from a compiler can assert on module structure directly, checking that the right functions are exported with the right types, without shelling out to wasm2wat and parsing text.

The Broader Context

This release fits into a broader pattern in Go’s relationship with WebAssembly. Go 1.21’s WASI support made Go programs viable as WASM plugins for host runtimes like wazero, which is itself a pure-Go WASM runtime. wazero has been doing for WASM execution what watgo now does for WASM tooling: making the whole stack available in Go without system dependencies. The combination of wazero for execution and watgo for module introspection and manipulation gives Go developers a complete WASM workflow that stays entirely within the Go ecosystem.

watgo is a relatively early release; the feature surface will grow as the WebAssembly spec evolves and as the Component Model, which wasm-tools already supports, becomes more central to the WASM ecosystem. But the core functionality, parse, validate, encode, decode, covers the vast majority of what most Go tools actually need from WASM file handling. Starting with a clean semantic IR and zero-dependency design from the beginning is the right foundation to build on.

For Go developers who’ve been reaching for wabt or wasm-tools as external dependencies in their build pipelines, it’s worth taking a look at what the watgo repository currently offers. The core use cases are covered, the API is clean, and the installation story is exactly what you’d want from a Go library.

Was this interesting?