Go has had WebAssembly as a compilation target since 2018. It can run WASM modules with Wazero. What it has always lacked is a native way to process WASM modules as data: parse them, inspect them, validate them, transform them, produce them from scratch. That gap is what watgo closes.
Eli Bendersky released watgo as a pure-Go, zero-dependency toolkit for working with WebAssembly modules. It handles the four core operations any toolchain needs: parsing WAT (the text format) to an in-memory representation, validating that representation against the WebAssembly specification, encoding it to binary WASM, and decoding binary WASM back into that representation. This covers the same ground as wabt in C++ and wasm-tools in Rust, but without requiring either language’s toolchain.
The Gap This Fills
Go’s relationship with WebAssembly has always been lopsided. The compilation side is mature: GOOS=js GOARCH=wasm has been there since Go 1.11, WASI Preview 1 support landed in Go 1.21, and TinyGo produces compact binaries for edge and embedded targets. Execution is covered by Wazero, which is pure Go with no CGo dependency.
The missing piece was the processing side. Before watgo, a Go program that needed to work with WASM modules as artifacts had three options, none of them satisfying.
The first was shelling out to external binaries, calling wat2wasm or wasm-tools as subprocesses. This works until it does not: binary availability becomes part of your deployment, cross-compilation gets complicated, and the subprocess boundary adds friction to anything that needs tight integration with Go’s type system.
The second was CGo bindings into wabt or wasm-tools. CGo removes subprocess overhead, but it breaks Go’s cross-compilation story entirely, requires a C or Rust toolchain at build time, and imposes cgo’s link-time complexity on every downstream user. Projects that have migrated from CGo to pure Go for libraries like SQLite drivers know exactly what is at stake here.
The third was reimplementing from scratch. WAT parsing is not just s-expression parsing. The text format requires understanding WebAssembly’s stack machine semantics, handling folded and flat instruction forms, resolving identifier scoping, and tracking control flow structure. Doing that correctly is a significant undertaking, not something you want to redo per-project.
watgo gives all of this as a library.
What the API Looks Like
The central abstraction is wasmir, a semantic in-memory representation of a WebAssembly module. This is not a raw parse tree. It is a structured Go model that corresponds to the module structure as the WebAssembly specification defines it: types, imports, functions, tables, memories, globals, exports, code, and data, linked into a Go struct tree rather than held together with integer indices.
package main
import (
"fmt"
"github.com/eliben/watgo"
"github.com/eliben/watgo/wasmir"
)
const wasmText = `
(module
(func (export "add") (param i32) (param 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 _, f := range mod.Functions {
fmt.Printf("function: %d instructions\n", len(f.Body))
}
}
The binary format stores all cross-references as integer indices: a function declaration references its type by index into the type section, instructions reference locals by index, and so on. wasmir resolves those indices into direct struct references, so traversal and analysis code reads naturally without index bookkeeping.
The CLI mirrors this, with an interface designed to be compatible with wasm-tools:
go install github.com/eliben/watgo/cmd/watgo@latest
watgo parse stack.wat -o stack.wasm
Because it is pure Go, installing the CLI requires nothing beyond the Go toolchain. Cross-compiling it for Linux ARM from a Mac involves setting GOOS and GOARCH, nothing else.
Design Decisions: IR vs. Streaming
The two existing toolkits made different architectural choices. wabt builds a full in-memory tree, which is ergonomic for analysis and transformation. wasm-tools uses a streaming, zero-copy parser that emits events as bytes are read, closer to a SAX XML parser than a DOM parser. The streaming approach is faster and more memory-efficient for large binaries, because it avoids materializing the entire module at once.
watgo follows wabt’s approach: full in-memory tree. This is the right call for a Go library. The encoding/json and encoding/xml packages both offer streaming APIs, but the tree-based json.Unmarshal is what most code uses. Library consumers expect to receive a value they can navigate, not an event stream they have to accumulate themselves. WebAssembly binaries are also not typically large; the concerns that motivate wasm-tools’ streaming design apply to Wasmtime-scale runtimes processing thousands of modules at runtime, not to build tooling.
watgo’s choice also enables something wasm-tools’ streaming parser makes harder: programmatic module construction and transformation. Read a binary, modify the struct tree, encode back to binary. That pattern is natural with a full IR, and it is why watgo is useful beyond format conversion.
Validation Is Not Optional Checking
One part of the library worth understanding carefully is validation. WebAssembly validation is defined formally in the specification as a type system over the stack machine. It is not syntactic sanity checking.
The validator must perform abstract interpretation over every code path in every function, tracking operand stack types at every branch target and verifying they are consistent. The unreachable instruction creates a polymorphic stack state that has to be handled correctly through nested control flow. Type mismatches at branch targets, references to non-existent functions or memories, imbalanced stack depths: these are all things validation catches before a runtime sees the module.
A validator that misses edge cases produces false positives: modules it accepts that conforming runtimes reject. For a pipeline that validates WASM before storage or execution, that false positive rate is what matters. watgo’s coverage appears to be complete for the core MVP semantics plus common proposals; SIMD, threads, GC, and newer exception handling variants may have gaps at this stage of the project.
How It Compares to wabt and wasm-tools
Neither wabt nor wasm-tools are usable from Go without friction. wabt is C++ and requires CGo to embed; wasm-tools is Rust and presents the same challenge. Both can be called as subprocesses, but that is subprocess integration, not library integration.
The Rust community had the same situation with C++‘s wabt in the early years of WebAssembly, and eventually the Bytecode Alliance built wasm-tools natively in Rust rather than wrapping wabt. The result is tighter integration with Rust’s build ecosystem, better composability within Rust programs, and no CGo-equivalent overhead. watgo is the same bet applied to Go.
The feature gap relative to the more mature tools is real but expected for an initial release. wabt has an interpreter, a decompiler, and WASM-to-C compilation. wasm-tools has comprehensive Component Model support, WIT parsing, and WASI 0.2 tooling built on top of it. watgo has none of these. For teams working specifically with the Component Model, wasm-tools remains necessary. For core WASM module processing in Go programs, watgo is now the native answer.
Go’s Binary Format Tradition
The Go standard library has long treated important binary formats as worthy of native libraries: debug/elf, debug/dwarf, debug/macho, debug/pe for native binaries; archive/zip and archive/tar for archives; image/png and image/jpeg for images. These packages share a design philosophy: implement the format faithfully in pure Go, expose types that correspond to the specification’s concepts, require no external dependencies.
The debug packages are all read-only, which limits what you can do with them. watgo is read-write, encoding back to binary format as well as decoding, because WASM tooling genuinely requires it. You cannot build a compiler backend, a module linker, or a post-processing pipeline on a read-only library.
WebAssembly was the obvious gap in Go’s binary format coverage. watgo closes it in a way that fits how the Go ecosystem works: you add it to your module with go get, cross-compile it freely, distribute a single static binary, and get a stable API in return. The project is early, and the Component Model frontier remains to be addressed, but the foundation is now there. Go had the runtime story and the compilation story; it finally has the tooling story too.