WebAssembly tooling has, until recently, lived primarily in two worlds: C++ and Rust. The WebAssembly Binary Toolkit (wabt) has been the reference implementation for years, written in C++ and maintained as part of the WebAssembly community group. More recently, the Bytecode Alliance released wasm-tools, a Rust-based toolkit that covers similar ground while also tracking newer specifications like the WebAssembly Component Model. Go, despite having first-class WASM compilation support via GOARCH=wasm and a thriving ecosystem around tools like TinyGo, has lacked a native library for working with WebAssembly artifacts as data. Eli Bendersky’s watgo closes that gap.
What the Gap Actually Looked Like
If you write Go and you want to parse a WAT file or validate a WASM binary, your options before watgo were not great. You could shell out to wat2wasm or wasm-validate from wabt, which works but ties you to an external binary, complicates cross-compilation, and makes your tool awkward to distribute. You could reach for a CGo wrapper around wabt’s C API, which introduces CGo’s well-known costs: slower build times, no cross-compilation without a toolchain, and a dependency chain that many Go developers actively avoid. You could use one of the partial Go WASM parsers that exist in the ecosystem, most of which are embedded inside larger runtimes like wazero and not designed to be used as standalone libraries.
None of these are satisfying when what you actually want is to read a WAT file, inspect its structure, and emit a binary, all from Go code with no external dependencies.
watgo’s Design
watgo solves this by providing four operations: parse, validate, encode, and decode. The central piece is wasmir, an intermediate representation that semantically models a WebAssembly module. Every operation either produces or consumes a wasmir value.
Parsing takes WAT (the WebAssembly Text format, an S-expression syntax defined in the official spec) and produces a wasmir module. Encoding takes a wasmir module and produces a WASM binary. Decoding goes the other direction: read a WASM binary and reconstruct a wasmir module from it. Validation runs the official WebAssembly validation rules against a wasmir module to confirm it is well-formed and type-safe.
The key design decision is that wasmir is not a parse tree. It is a semantic representation: after parsing, the module is normalized into a form that reflects the actual structure of a WebAssembly module as defined by the spec, not the syntactic quirks of the text format. This means you can construct or manipulate modules programmatically, not just round-trip files.
Here is what working with the API looks like:
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)
}
// mod is a *wasmir.Module
for _, fn := range mod.Funcs {
fmt.Printf("function with %d params\n", len(fn.Type.Params))
}
}
The API is structured around the watgo package for the high-level operations and wasmir for the types. This mirrors how you’d expect a Go library to be organized: the top-level package gives you the verbs, the sub-package gives you the nouns.
The Zero-Dependency Decision
The library is pure Go with zero external dependencies. In Go, this is a deliberate and consequential choice, not a default state. The standard library is extensive enough that you can build a surprising amount without pulling in anything from the module graph, but a full WAT parser, a binary encoder/decoder, and a spec-compliant validator are not trivial. The fact that watgo implements all of this in pure Go is a statement about scope discipline.
Zero dependencies matter for several reasons in the Go ecosystem. Cross-compilation is the most practical one: pure Go code compiles for any target without needing a C toolchain. If you are building a tool that runs on Linux, macOS, and Windows, or that targets ARM alongside amd64, CGo is a serious friction point. A pure Go library sidesteps all of that.
There is also the supply chain consideration. Every dependency you add brings its transitive graph with it. For a library like watgo, which might be included in compilers, linters, or other developer tools, keeping the dependency surface zero means it can be pulled into any project without concern.
This is not unique to watgo: wazero, the pure-Go WebAssembly runtime, made the same choice and cites it prominently as a feature. The Go community has converged on zero-dependency or minimal-dependency as a mark of quality for foundational libraries.
The CLI
Beyond the library, watgo ships a CLI that aims for compatibility with wasm-tools. You install it with:
go install github.com/eliben/watgo/cmd/watgo@latest
And use it like this:
watgo parse stack.wat -o stack.wasm
The intent to be compatible with wasm-tools is significant. It means watgo can serve as a drop-in replacement for wasm-tools in Go-centric projects without requiring developers to change their tooling habits. It also signals that Bendersky is tracking the wasm-tools interface as a de facto standard, which makes sense: wasm-tools has become the reference CLI for component model work and is increasingly used in build pipelines outside of Rust projects.
Where This Fits in the Go WebAssembly Story
Go’s WebAssembly story has two main tracks. There is Go-compiled-to-WASM: the standard toolchain supports GOARCH=wasm GOOS=js and GOARCH=wasip1 GOOS=wasip1 (added in Go 1.21 for WASI Preview 1). TinyGo extends this further, producing smaller binaries suitable for embedded and serverless contexts. This track is about using WebAssembly as a compilation target.
The second track is Go-that-processes-WASM: programs written in Go that read, write, transform, or execute WebAssembly modules. Runtimes like wazero live here, as do tools that might analyze WASM for security scanning, optimize binaries, or generate bindings. watgo lives here too. Until now, this track lacked a solid, standalone library for the basic operations of parsing and encoding.
The addition of a high-quality wasmir IR is what makes watgo more than just a WAT-to-binary converter. If you want to build a WASM optimizer, a custom validator, or a source map tool in Go, you need a representation you can traverse and mutate. wabt has its own C API for this purpose, and wasm-tools exposes a Rust API. watgo gives Go the same capability.
Validation Semantics
One detail worth emphasizing: the validation step uses the official WebAssembly validation semantics, not an ad hoc sanity check. The WebAssembly spec’s validation chapter defines a type system for modules that must be satisfied before a module is safe to instantiate. This includes checking that operand stacks are balanced at every branch target, that type annotations on functions match their bodies, and that memory and table accesses reference indices that exist. Implementing this correctly is nontrivial. A validator that skips edge cases will accept modules that runtimes reject, which makes it useless for tooling that needs to produce correct output.
By claiming compliance with the spec’s validation semantics, watgo is taking on a meaningful obligation. It also means the validation step is suitable for use in build pipelines that need assurance before shipping a binary.
Conclusion
watgo does not do anything that wabt or wasm-tools cannot do. What it does is make those capabilities available to Go programs natively, without CGo, without shelling out, and without any external dependencies. For anyone building Go tools that work with WebAssembly artifacts, that is a meaningful change. The wasmir representation in particular opens up use cases beyond simple format conversion: programmatic module construction, analysis, and transformation are all now first-class options in Go.
The project is available at github.com/eliben/watgo. Given Bendersky’s track record of well-documented, well-tested Go libraries, it is a reasonable bet that the API will remain stable and the implementation will stay close to the spec.