Building components with Go
The Go ecosystem provides developers with powerful options for building WebAssembly components. Go and the TinyGo fork provide a strong foundation for development:
- TinyGo: The fast-moving implementation of Go for embedded environments includes native compilation to components with WASI 0.2 support as of TinyGo 0.33.
- Go has supported compilation to WebAssembly for years; we expect native WASI 0.2 support like that in TinyGo to land in the near future.
Go-native tooling
There are a number of useful tools available as Go packages that streamline and simplify the Go development experience:
wasm-tools-go
wasm-tools-go
is a project hosted by the Bytecode Alliance that collects utilities such as wit-bindgen-go
, which generates Go bindings for WebAssembly Interface Type (WIT) interfaces.
component-sdk-go
The Go Component SDK is an optional, open source framework that simplifies the development of WebAssembly components targeting the wasmCloud host runtime. The SDK aims to provide a more idiomatic Go development experience for developers using WASI interfaces such as wasi:http
or wasi:logging
.
wadge
wadge
is a "bridging" framework that enables your native code to interact with WebAssembly component interfaces for purposes such as testing. wadge
acts as a bridge between your Go toolchain and a WebAssembly runtime that makes interfaces “just work” when it's time to test your code.
Get started
In this walkthrough, we will create an HTTP server from scratch using the Go component SDK, write a test for the application, and use wadge
to run the test. This walkthrough requires:
- Go 1.23.0+
- TinyGo 0.33.0+
- wasmCloud Shell (
wash
) CLI 0.36.1+ for building and deploying components wasm-tools
for Go binding generation
Step 1: Set up with wash
The wasmCloud Shell (wash
) CLI helps developers build component-based applications that can be deployed with wasmCloud, and consolidates many open source component development tools.
Create a new project called http-test
and navigate into the project directory:
wash new component http-test --template-name hello-world-tinygo
cd http-test
Replace the contents of world.wit
with the WIT definition below:
package example:http-server;
world hello {
include wasmcloud:component-go/imports@0.1.0;
export wasi:http/incoming-handler@0.2.0;
}
Finally, we'll delete the contents of hello.go
and write our HTTP server, which will return a simple "hello world."
Using the Component SDK, this looks like a fairly standard server using the HTTP standard library, with only a handful of exceptions where we use methods of wasihttp
or wasilog
.
//go:generate go run github.com/bytecodealliance/wasm-tools-go/cmd/wit-bindgen-go generate --world hello --out gen ./wit
package main
import (
"net/http"
"go.wasmcloud.dev/component/log/wasilog"
"go.wasmcloud.dev/component/net/wasihttp"
)
func init() {
wasihttp.HandleFunc(handler)
}
func handler(w http.ResponseWriter, r *http.Request) {
logger := wasilog.ContextLogger("handler")
logger.Info("request received", "host", r.Host, "path", r.URL.Path, "agent", r.Header.Get("User-Agent"))
_, err := w.Write([]byte("hello world!"))
if err != nil {
logger.Error("failed to write body", "error", err)
}
}
func main() {}
Add a tools.go
file in your project so that the wit-bindgen-go
tooling is able to generate the necessary bindings:
touch tools.go
//go:build tools
package main
import (
_ "github.com/bytecodealliance/wasm-tools-go/cmd/wit-bindgen-go"
)
Download missing packages:
go mod tidy
Replace the contents of wasmcloud.toml
with the configuration below:
name = "http-hello-world"
language = "tinygo"
type = "component"
version = "0.1.0"
[component]
wasm_target = "wasm32-wasi-preview2"
wit_world = "hello"
We also need a folder where the generated bindings will be stored:
mkdir gen
When we run wash build
, we will generate bindings and compile a component:
wash build
The wash inspect
subcommand enables us to examine the new component's imports and exports:
wash inspect --wit build/http_hello_world_s.wasm
package root:component;
world root {
import wasi:clocks/monotonic-clock@0.2.0;
import wasi:io/error@0.2.0;
import wasi:io/streams@0.2.0;
import wasi:http/types@0.2.0;
import wasi:logging/logging;
import wasi:cli/environment@0.2.0;
import wasi:cli/stdin@0.2.0;
import wasi:cli/stdout@0.2.0;
import wasi:cli/stderr@0.2.0;
import wasi:clocks/wall-clock@0.2.0;
import wasi:filesystem/types@0.2.0;
import wasi:filesystem/preopens@0.2.0;
import wasi:random/random@0.2.0;
export wasi:http/incoming-handler@0.2.0;
}
Now we can deploy on wasmCloud and try the component manually.
- Start a developer loop with
wash dev
In another tab, we can curl
the application and you should see the "hello world" response:
$ curl localhost:8000
hello world!
Step 2: Testing with a WASI interface
wadge
is a "bridging" framework that will enable us to test our code using go test
and standard test syntax.
Use go get
to add wadge
to the project:
go get go.wasmcloud.dev/wadge
Add a tools.go
file to include the wadge
bindgen:
touch tools.go
//go:build tools
package main
import (
_ "github.com/bytecodealliance/wasm-tools-go/cmd/wit-bindgen-go"
_ "go.wasmcloud.dev/wadge/cmd/wadge-bindgen-go"
)
Now we will write a test for the application in a new file called hello_test.go
:
touch hello_test.go
package main
import (
"io"
"log"
"log/slog"
"net/http"
"os"
"testing"
"github.com/stretchr/testify/assert"
incominghandler "go.wasmcloud.dev/component/gen/wasi/http/incoming-handler"
"go.wasmcloud.dev/wadge"
"go.wasmcloud.dev/wadge/wadgehttp"
)
func init() {
log.SetFlags(0)
slog.SetDefault(slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{
Level: slog.LevelDebug, ReplaceAttr: func(groups []string, a slog.Attr) slog.Attr {
if a.Key == slog.TimeKey {
return slog.Attr{}
}
return a
},
})))
}
func TestIncomingHandler(t *testing.T) {
wadge.RunTest(t, func() {
req, err := http.NewRequest("", "/", nil)
if err != nil {
t.Fatalf("failed to create new HTTP request: %s", err)
}
resp, err := wadgehttp.HandleIncomingRequest(incominghandler.Exports.Handle, req)
if err != nil {
t.Fatalf("failed to handle incoming HTTP request: %s", err)
}
assert.Equal(t, 200, resp.StatusCode)
assert.Equal(t, http.Header{}, resp.Header)
buf, err := io.ReadAll(resp.Body)
if err != nil {
t.Fatalf("failed to read HTTP response body: %s", err)
}
assert.Equal(t, []byte("hello world!"), buf)
})
}
go mod download && go mod tidy
Generate wadge
bindings for your test:
go run go.wasmcloud.dev/wadge/cmd/wadge-bindgen-go
This generates bindings.wadge.go
.
Now run go test
:
go test
level=DEBUG msg="reading response body buffer"
level=DEBUG msg="read body stream chunk" buf="hello world!"
level=DEBUG msg="reading response body buffer"
level=DEBUG msg="response body closed"
PASS
ok github.com/wasmcloud/wasmcloud/examples/golang/components/http-hello-world 0.299s
To clean up, stop wash dev
with CTRL+C.
Reference: Set up without wash
wash
simplifies set up for a Go project, but it is not required to build a Go-based component. For users who wish to use or understand the underlying tooling, the steps below replicate the initial set up above without wash
.
You'll also need to install the wkg
cli to fetch your component's interface dependencies. Installation instructions can be found here.
Once that is installed, run the following command to edit your wasm package tools configuration wkg
to know the location of wasmcloud
namespaced interface dependencies:
wkg config edit
Replace all content in that file with the following:
[namespace_registries]
wasmcloud = "wasmcloud.com"
wasi = "wasi.dev"
Create a new Go project:
mkdir http-test && cd http-test
go mod init example/http/test
Now we'll add the Component SDK and wasilog
packages to our project:
go get go.wasmcloud.dev/component go.wasmcloud.dev/component/log/wasilog
Create a wit/
directory and a file in that directory called world.wit
:
mkdir wit && touch ./wit/world.wit
package example:http-server;
world hello {
include wasmcloud:component/imports@0.2.0-draft;
export wasi:http/incoming-handler@0.2.0;
}
Add the wasm-tools-go
package to a tools.go
file in your project:
touch tools.go
//go:build tools
package main
import (
_ "github.com/bytecodealliance/wasm-tools-go/cmd/wit-bindgen-go"
)
Then update your Go module:
go mod tidy
Then fetch your dependencies:
wkg wit fetch
Finally, we'll write our HTTP server in a new hello.go
file. Using the Component SDK, this looks like a standard server using the HTTP standard library.
touch hello.go
//go:generate go run github.com/bytecodealliance/wasm-tools-go/cmd/wit-bindgen-go generate --world hello --out gen ./wit
package main
import (
"net/http"
"go.wasmcloud.dev/component/log/wasilog"
"go.wasmcloud.dev/component/net/wasihttp"
)
func init() {
wasihttp.HandleFunc(handler)
}
func handler(w http.ResponseWriter, r *http.Request) {
logger := wasilog.ContextLogger("handler")
logger.Info("request received", "host", r.Host, "path", r.URL.Path, "agent", r.Header.Get("User-Agent"))
_, err := w.Write([]byte("hello world!"))
if err != nil {
logger.Error("failed to write body", "error", err)
}
}
func main() {}
Now we'll generate Go bindings for the application's WIT interfaces.
mkdir gen
go generate
At this point, you can compile your Wasm component using tinygo build
:
tinygo build --target=wasip2 --wit-package ./wit --wit-world hello
Next steps
- Explore capabilities you can use in your wasmCloud application.
- Learn how to build a capability provider in Go.