Skip to main content
Version: 1.x

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:

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:

shell
wash new component http-test --template-name hello-world-tinygo
shell
cd http-test

Replace the contents of world.wit with the WIT definition below:

wit
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
//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:

shell
touch tools.go
go
//go:build tools

package main

import (
	_ "github.com/bytecodealliance/wasm-tools-go/cmd/wit-bindgen-go"
)

Download missing packages:

shell
go mod tidy

Replace the contents of wasmcloud.toml with the configuration below:

toml
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:

shell
mkdir gen

When we run wash build, we will generate bindings and compile a component:

shell
wash build

The wash inspect subcommand enables us to examine the new component's imports and exports:

shell
wash inspect --wit build/http_hello_world_s.wasm
wit
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:

console
$ 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:

shell
go get go.wasmcloud.dev/wadge

Add a tools.go file to include the wadge bindgen:

shell
touch tools.go
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:

shell
touch hello_test.go
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)
	})
}
shell
go mod download && go mod tidy

Generate wadge bindings for your test:

shell
go run go.wasmcloud.dev/wadge/cmd/wadge-bindgen-go

This generates bindings.wadge.go.

Now run go test:

shell
go test
text
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:

shell
wkg config edit

Replace all content in that file with the following:

toml
[namespace_registries]
wasmcloud = "wasmcloud.com"
wasi = "wasi.dev"

Create a new Go project:

shell
mkdir http-test && cd http-test
shell
go mod init example/http/test

Now we'll add the Component SDK and wasilog packages to our project:

shell
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:

shell
mkdir wit && touch ./wit/world.wit
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:

shell
touch tools.go
go
//go:build tools

package main

import (
	_ "github.com/bytecodealliance/wasm-tools-go/cmd/wit-bindgen-go"
)

Then update your Go module:

shell
go mod tidy

Then fetch your dependencies:

shell
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.

shell
touch hello.go
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.

shell
mkdir gen
shell
go generate

At this point, you can compile your Wasm component using tinygo build:

shell
tinygo build --target=wasip2 --wit-package ./wit --wit-world hello

Next steps