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. Since the project moves quickly, we always recommend using the latest version of TinyGo.
- 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.
wasmCloud/Go is a repository containing Go ecosystem libraries and examples for wasmCloud. This repo serves as a centralized home for resources including the Component SDK package, component templates, component examples, and more.
Go-native tooling
There are a number of useful tools available as Go packages that streamline and simplify the Go development experience:
go.bytecodealliance.org
go.bytecodealliance.org (GitHub | go.pkg.dev) is a Go package hosted by the Bytecode Alliance that collects utilities such as wit-bindgen-go, which generates Go bindings for WebAssembly Interface Type (WIT) interfaces.
go.wasmcloud.dev/component
The Go Component SDK (GitHub | go.pkg.dev) 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.
go.wasmcloud.dev/wadge
wadge (GitHub | go.pkg.dev) 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-toolsv1.225.0 for Go binding generation
Due to incompatibilities introduced in wasm-tools v1.226.0 and higher, we strongly recommend using wasm-tools v1.225.0 when building Go projects. The easiest way to download wasm-tools v1.225.0 is via cargo: cargo install --locked wasm-tools@1.225.0
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-tinygocd http-testReplace 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 main.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 go run go.bytecodealliance.org/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 (
_ "go.bytecodealliance.org/cmd/wit-bindgen-go"
)Download missing packages:
go mod tidyReplace 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"When we run wash build, we will generate bindings and compile a component:
wash buildThe wash inspect subcommand enables us to examine the new component's imports and exports:
wash inspect --wit build/http_hello_world_s.wasmpackage 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/wadgeAdd a tools.go file to include the wadge bindgen:
touch tools.go//go:build tools
package main
import (
_ "go.bytecodealliance.org/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.gopackage 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 tidyGenerate wadge bindings for your test:
go run go.wasmcloud.dev/wadge/cmd/wadge-bindgen-goThis generates bindings.wadge.go.
Now run go test:
go testlevel=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.299sTo 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 editReplace 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-testgo mod init example/http/testNow 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.witpackage example:http-server;
world hello {
include wasmcloud:component/imports@0.2.0-draft;
export wasi:http/incoming-handler@0.2.0;
}Add the go.bytecodealliance.org package to a tools.go file in your project:
touch tools.go//go:build tools
package main
import (
_ "go.bytecodealliance.org/cmd/wit-bindgen-go"
)Then update your Go module:
go mod tidyThen fetch your dependencies:
wkg wit fetchFinally, 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 go run go.bytecodealliance.org/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 gengo generateAt this point, you can compile your Wasm component using tinygo build:
tinygo build --target=wasip2 --wit-package ./wit --wit-world helloNext steps
- Explore capabilities you can use in your wasmCloud application.
- Learn how to build a capability provider in Go.