Skip to content
下载《AI 应用 & AI Agent 开发新范式》电子书 了解构建 AI Agent 和 MCP Server 的一线实践Know more

Developing WASM Plugins with Go

Note:

TinyGo has specific version requirements. The current stable version combination that has been extensively validated is: TinyGo 0.29 + Go 1.20. You can refer to this official Makefile

Go 1.24 now natively supports compiling WASM files. Documentation updates are in progress.

1. Tool Preparation

You need to install both Golang and TinyGo.

1. Golang

(Requires version 1.18 or higher)
Official installation guide: https://go.dev/doc/install

Windows

  1. Download the installer: https://go.dev/dl/go1.19.windows-amd64.msi
  2. Run the downloaded installer. By default, it will be installed in the Program Files or Program Files (x86) directory
  3. After installation, press “Win+R” to open the Run dialog, type “cmd” and press Enter to open the command prompt. Then type: go version to verify the installation

macOS

  1. Download the installer: https://go.dev/dl/go1.19.darwin-amd64.pkg
  2. Run the downloaded installer. By default, it will be installed in the /usr/local/go directory
  3. Open Terminal and type: go version to verify the installation

Linux

  1. Download the archive: https://go.dev/dl/go1.19.linux-amd64.tar.gz
  2. Run the following commands to install:
Terminal window
rm -rf /usr/local/go && tar -C /usr/local -xzf go1.19.linux-amd64.tar.gz
export PATH=$PATH:/usr/local/go/bin
  1. Type go version to verify the installation

2. TinyGo

(Requires version 0.28.1 or higher)
Official installation guide: https://tinygo.org/getting-started/install/

Windows

  1. Download the installer: https://github.com/tinygo-org/tinygo/releases/download/v0.28.1/tinygo0.28.1.windows-amd64.zip
  2. Extract the archive to your desired directory
  3. If you extracted to C:\tinygo, add C:\tinygo\bin to your PATH environment variable, for example by running:
Terminal window
set PATH=%PATH%;"C:\tinygo\bin";
  1. Open a command prompt and type tinygo version to verify the installation

macOS

  1. Download and extract the archive:
Terminal window
wget https://github.com/tinygo-org/tinygo/releases/download/v0.28.1/tinygo0.28.1.darwin-amd64.tar.gz
tar -zxf tinygo0.28.1.darwin-amd64.tar.gz
  1. If you extracted to /tmp, add /tmp/tinygo/bin to your PATH:
Terminal window
export PATH=/tmp/tinygo/bin:$PATH
  1. Open Terminal and type tinygo version to verify the installation

Linux

For Ubuntu on amd64 architecture (other systems please refer to the official guide):

  1. Download and install the DEB package:
Terminal window
wget https://github.com/tinygo-org/tinygo/releases/download/v0.28.1/tinygo_0.28.1_amd64.deb
sudo dpkg -i tinygo_0.28.1_amd64.deb
export PATH=$PATH:/usr/local/bin
  1. Open Terminal and type tinygo version to verify the installation
  2. After completed the installation, open “Run” dialog with hotkey “Win+R”. Type “cmd” in the dialog and click “OK” to open Command Line Prompt. Type: go version. If version info is displayed, the package has been successfully installed.

MacOS

  1. Download the installer: https://go.dev/dl/go1.19.darwin-amd64.pkg
  2. Run the downloaded installer to start the installation. It will be installed to /usr/local/go folder by default.
  3. Open Terminal and type: go version. If version info is displayed, the package has been successfully installed.

Linux

  1. Download the installer: https://go.dev/dl/go1.19.linux-amd64.tar.gz
  2. Execute following commands to start the installation:
Terminal window
rm -rf /usr/local/go && tar -C /usr/local -xzf go1.19.linux-amd64.tar.gz
export PATH=$PATH:/usr/local/go/bin
  1. Execute go version. If version info is displayed, the package has been successfully installed.

2. TinyGo

Min Version: 0.28.1
Official download link: https://tinygo.org/getting-started/install/

Windows

  1. Download the package: https://github.com/tinygo-org/tinygo/releases/download/v0.28.1/tinygo0.28.1.windows-amd64.zip
  2. Unpack the package to the target folder
  3. If the package is unpacked to folder C:\tinygo, you need to add C:\tinygo\bin into the environment variable PATH, using set command in Command Line Prompt for example.
Terminal window
set PATH=%PATH%;"C:\tinygo\bin";
  1. Execute tinygo version command in Command Line Prompt. If version info is displayed, the package has been successfully installed.

MacOS

  1. Download and unpack the package
Terminal window
wget https://github.com/tinygo-org/tinygo/releases/download/v0.28.1/tinygo0.28.1.darwin-amd64.tar.gz
tar -zxf tinygo0.28.1.darwin-amd64.tar.gz
  1. If the package is unpacked to folder /tmp, you need to add /tmp/tinygo/bin to the environment variable PATH:
Terminal window
export PATH=/tmp/tinygo/bin:$PATH
  1. Execute command tinygo version in Terminal. If version info is displayed, the package has been successfully installed.

Linux

Following steps are based on Ubuntu AMD64. For other OSes, please refer to the official document.

  1. Download and install the DEB package.
Terminal window
wget https://github.com/tinygo-org/tinygo/releases/download/v0.28.1/tinygo_0.28.1_amd64.deb
sudo dpkg -i tinygo_0.28.1_amd64.deb
export PATH=$PATH:/usr/local/bin
  1. Execute command tinygo version in Terminal. If version info is displayed, the package has been successfully installed.

2. Write a plugin

1. Initialize the project

You can create your wasm-go plugin directory in the repo higress’s plugins/wasm-go that you can use the scaffolding tools provided in this directory(see 1.1); or create a new directory for your Go project yourself(see 1.2). If you are developing wasm-go plugins for the first time, it is recommended to take the former.

1.1 create wasm-go plugin in plugins/wasm-go

  1. git clone https://github.com/alibaba/higress.git, to clone project to local;
  2. cd plugins/wasm-go; mkdir wasm-demo-go, to go to the project’s plugins/wasm-go directory and create the wasm-demo-go directory.

1.2 create a new project yourself

  1. Create a new folder for the project. For example: wasm-demo-go.
  2. Execute following commands in the new folder to initialize the Go project:
Terminal window
go mod init wasm-demo-go
  1. If you are in the Chinese mainland, you may need to set a proxy for downloading dependencies.
Terminal window
go env -w GOPROXY=https://proxy.golang.com.cn,direct
  1. Download dependencies for plugin building.
Terminal window
go get github.com/tetratelabs/proxy-wasm-go-sdk
go get github.com/alibaba/higress/plugins/wasm-go@main
go get github.com/tidwall/gjson

2. Writing the Plugin

1. Initialize the Project

  1. Create a new project directory, for example wasm-demo-go
  2. In the created directory, run the following command to initialize a Go module:
Terminal window
go mod init wasm-demo-go
  1. For users in China, you may need to set up a proxy for downloading dependencies:
Terminal window
go env -w GOPROXY=https://proxy.golang.com.cn,direct
  1. Download the required dependencies for building the plugin:
Terminal window
go get github.com/higress-group/proxy-wasm-go-sdk
go get github.com/alibaba/higress/plugins/wasm-go@main
go get github.com/tidwall/gjson

2. Writing main.go

Below is a simple example that implements the following functionality:

  • When the plugin is configured with mockEnable: true, it directly returns a “hello world” response
  • When no plugin configuration is provided or mockEnable: false, it adds a hello: world request header to the original request

Note: The plugin configuration in the gateway console is in YAML format, but it will be automatically converted to JSON format when delivered to the plugin. Therefore, the example directly parses the configuration from JSON.

package main
import (
"github.com/alibaba/higress/plugins/wasm-go/pkg/wrapper"
"github.com/higress-group/proxy-wasm-go-sdk/proxywasm"
"github.com/higress-group/proxy-wasm-go-sdk/proxywasm/types"
"github.com/tidwall/gjson"
)
func main() {
wrapper.SetCtx(
// Plugin name
"my-plugin",
// Custom function for parsing plugin configuration
wrapper.ParseConfigBy(parseConfig),
// Custom function for processing request headers
wrapper.ProcessRequestHeadersBy(onHttpRequestHeaders),
)
}
// Custom plugin configuration
type MyConfig struct {
mockEnable bool
}
// The YAML configuration from the console is automatically converted to JSON
// We can directly parse the configuration from the json parameter
func parseConfig(json gjson.Result, config *MyConfig, log wrapper.Log) error {
// Parse the configuration and update the config object
config.mockEnable = json.Get("mockEnable").Bool()
return nil
}
func onHttpRequestHeaders(ctx wrapper.HttpContext, config MyConfig, log wrapper.Log) types.Action {
proxywasm.AddHttpRequestHeader("hello", "world")
if config.mockEnable {
proxywasm.SendHttpResponse(200, nil, []byte("hello world"), -1)
}
return types.ActionContinue
}

HTTP Processing Hooks

In the example above, we used wrapper.ProcessRequestHeadersBy to register our custom function onHttpRequestHeaders to handle requests during the “HTTP Request Headers” phase. You can also register handlers for other phases using the following methods:

HTTP Processing PhaseTriggerHook Method
HTTP Request HeadersWhen the gateway receives request headers from the clientwrapper.ProcessRequestHeadersBy
HTTP Request BodyWhen the gateway receives request body from the clientwrapper.ProcessRequestBodyBy
HTTP Response HeadersWhen the gateway receives response headers from the backendwrapper.ProcessResponseHeadersBy
HTTP Response BodyWhen the gateway receives response body from the backendwrapper.ProcessResponseBodyBy

Utility Methods

The example uses proxywasm.AddHttpRequestHeader and proxywasm.SendHttpResponse, which are utility methods provided by the SDK. Here are the main utility methods available:

CategoryMethodDescriptionApplicable HTTP Processing Phases
Request HeadersGetHttpRequestHeadersGet all request headersHTTP Request Headers
ReplaceHttpRequestHeadersReplace all request headersHTTP Request Headers
GetHttpRequestHeaderGet a specific request headerHTTP Request Headers
RemoveHttpRequestHeaderRemove a specific request headerHTTP Request Headers
ReplaceHttpRequestHeaderReplace a specific request headerHTTP Request Headers
AddHttpRequestHeaderAdd a new request headerHTTP Request Headers
Request BodyGetHttpRequestBodyGet the request bodyHTTP Request Body
AppendHttpRequestBodyAppend data to the end of the request bodyHTTP Request Body
PrependHttpRequestBodyAdd data to the beginning of the request bodyHTTP Request Body
ReplaceHttpRequestBodyReplace the entire request bodyHTTP Request Body
Response HeadersGetHttpResponseHeadersGet all response headersHTTP Response Headers
ReplaceHttpResponseHeadersReplace all response headersHTTP Response Headers
GetHttpResponseHeaderGet a specific response headerHTTP Response Headers
RemoveHttpResponseHeaderRemove a specific response headerHTTP Response Headers
ReplaceHttpResponseHeaderReplace a specific response headerHTTP Response Headers
AddHttpResponseHeaderAdd a new response headerHTTP Response Headers
Response BodyGetHttpResponseBodyGet the response bodyHTTP Response Body
AppendHttpResponseBodyAppend data to the end of the response bodyHTTP Response Body
PrependHttpResponseBodyAdd data to the beginning of the response bodyHTTP Response Body
ReplaceHttpResponseBodyReplace the entire response bodyHTTP Response Body
HTTP CallsDispatchHttpCallSend an HTTP request-
GetHttpCallResponseHeadersGet response headers from DispatchHttpCall-
GetHttpCallResponseBodyGet response body from DispatchHttpCall-
GetHttpCallResponseTrailersGet response trailers from DispatchHttpCall-
Direct ResponseSendHttpResponseReturn a specific HTTP response-
Flow ControlResumeHttpRequestResume a previously paused request-
ResumeHttpResponseResume a previously paused response-
  1. If mockEnable is set to true, send hello world directly as the response.
  2. If mockEnable is not set or set to false, add an extra HTTP header hello: world to the original request. More samples can be found in section 4 below.

Note: Plugin configurations use YAML format in the gateway console. But plugins receive them in JSON format. So in the sample below, actual config data are extracted from JSON by the parseConfig function.

package main
import (
"github.com/alibaba/higress/plugins/wasm-go/pkg/wrapper"
"github.com/tetratelabs/proxy-wasm-go-sdk/proxywasm"
"github.com/tetratelabs/proxy-wasm-go-sdk/proxywasm/types"
"github.com/tidwall/gjson"
)
func main() {
wrapper.SetCtx(
// Plugin name
"my-plugin",
// A custom function for parsing plugin configurations
wrapper.ParseConfigBy(parseConfig),
// A custom function for processing request headers
wrapper.ProcessRequestHeadersBy(onHttpRequestHeaders),
)
}
// Custom plugin configuration
type MyConfig struct {
mockEnable bool
}
// Plugin configurations set in the console with YAML format will be converted to JSON. So we just need to parse config data from JSON.
func parseConfig(json gjson.Result, config *MyConfig, log wrapper.Log) error {
// Get the configuration property and set to the config object.
config.mockEnable = json.Get("mockEnable").Bool()
return nil
}
func onHttpRequestHeaders(ctx wrapper.HttpContext, config MyConfig, log wrapper.Log) types.Action {
proxywasm.AddHttpRequestHeader("hello", "world")
if config.mockEnable {
proxywasm.SendHttpResponse(200, nil, []byte("hello world"), -1)
}
return types.ActionContinue
}

HTTP Processing Pointcuts

In the sample above, wrapper.ProcessRequestHeadersBy applies custom function onHttpRequestHeaders when processing requests inHTTP request header processing stage. Besides that, you can use following methods to set custom processing functions for various stages.

HTTP Processing StageTrigger TimePointcut Mounting Method
HTTP request header processing stageWhen gateway receives request headers from clientwrapper.ProcessRequestHeadersBy
HTTP request body processing stageWhen gateway receives request body from clientwrapper.ProcessRequestBodyBy
HTTP response header processing stageWhen gateway receives response headers from upstreamwrapper.ProcessResponseHeadersBy
HTTP response body processing stageWhen gateway receives response body from upstreamwrapper.ProcessResponseBodyBy

Utility Functions

In the sample above, proxywasm.AddHttpRequestHeader and proxywasm.SendHttpResponse are two utility methods provided by the plugin SDK. You can find major utility functions in the table below:

CategoryNameUsageAvailable
HTTP Processing Stage(s)
Request Header ProcessingGetHttpRequestHeadersGet all the request headers sent by the clientHTTP request header processing stage
ReplaceHttpRequestHeadersReplace all headers in the request.HTTP request header processing stage
GetHttpRequestHeaderGet the specified header in the request.HTTP request header processing stage
RemoveHttpRequestHeaderRemove the specified header from the request.HTTP request header processing stage
ReplaceHttpRequestHeaderReplace the specified header in the response.HTTP request header processing stage
AddHttpRequestHeaderAdd a new header to the request.HTTP request header processing stage
Request Body ProcessingGetHttpRequestBodyGet the request body received from client.HTTP request body processing stage
AppendHttpRequestBodyAppend the specified binary data to the request body.HTTP request body processing stage
PrependHttpRequestBodyPrepend the specified binary data to the request body.HTTP request body processing stage
ReplaceHttpRequestBodyReplace the entire request body received from client.HTTP request body processing stage
Response Header ProcessingGetHttpResponseHeadersGet all the response headers received from upstream.HTTP response header processing stage
ReplaceHttpResponseHeadersReplace all headers in the response.HTTP response header processing stage
GetHttpResponseHeaderGet the specified header in the response.HTTP response header processing stage
RemoveHttpResponseHeaderRemove the specified header from the response.HTTP response header processing stage
ReplaceHttpResponseHeaderReplace the specified header in the response.HTTP response header processing stage
AddHttpResponseHeaderAdd a new header to the responseHTTP response headers processing stage
Response BodyGetHttpResponseBodyGet the response body received from the backendHTTP response body processing stage
AppendHttpResponseBodyAppend binary data to the end of the response bodyHTTP response body processing stage
PrependHttpResponseBodyAdd binary data to the beginning of the response bodyHTTP response body processing stage
ReplaceHttpResponseBodyReplace the entire response body with new dataHTTP response body processing stage
HTTP CallDispatchHttpCallSend an HTTP request.-
GetHttpCallResponseHeadersGet the response headers associated with a DispatchHttpCall call.-
GetHttpCallResponseBodyGet the response body associated with a DispatchHttpCall call.-
GetHttpCallResponseTrailersGet the response trailer associated with a DispatchHttpCall call.-
Respond DirectlySendHttpResponseReturn a specific HTTP response immediately.-
Process ResumingResumeHttpRequestResume the request processing workflow paused before.-
ResumeHttpResponseResume the response processing workflow paused before.-

3. Compile and Generate WASM File

Using proxy-wasm community version 0.2.1 ABI, in the HTTP request/response processing phases, you can only use types.ActionContinue and types.ActionPause as return values to control the flow.

  • If your project directory is in the plugins/wasm-go directory, see 3.1.
  • If you are using a self-initialized directory, see 3.2.

3.1 Building wasm-go plugin image with scaffolding

The wasm-go plugin can be built quickly with the following command:

Terminal window
$ PLUGIN_NAME=wasm-demo-go make build
... ...
image: wasm-demo-go:20230223-173305-3b1a471
output wasm file: extensions/wasm-demo-go/plugin.wasm

This command eventually builds a wasm file and a Docker image. This local wasm file is exported to the specified plugin’s directory and can be used directly for local debugging. You can also use make build-push to build and push the image together. See plugins/wasm-go for more.

3.2 Compile wasm files locally

Execute the following command:

Terminal window
tinygo build -o main.wasm -scheduler=none -target=wasi -gc=custom -tags='custommalloc nottinygc_finalizer' ./main.go

A new file named main.wasm will be created after a successful compilation, which will be used in the local debugging sample below as well.
When using custom plugin function in the cloud native gateway market, you just need to upload this file.

3. Local Debugging

TBD

More Samples

Plugin with No Configuration

If the plugin needs no configuration, just define an empty config struct.

package main
import (
"github.com/alibaba/higress/plugins/wasm-go/pkg/wrapper"
"github.com/tetratelabs/proxy-wasm-go-sdk/proxywasm"
"github.com/tetratelabs/proxy-wasm-go-sdk/proxywasm/types"
)
func main() {
wrapper.SetCtx(
"hello-world",
wrapper.ProcessRequestHeadersBy(onHttpRequestHeaders),
)
}
type MyConfig struct {}
func onHttpRequestHeaders(ctx wrapper.HttpContext, config MyConfig, log wrapper.Log) types.Action {
proxywasm.SendHttpResponse(200, nil, []byte("hello world"), -1)
return types.ActionContinue
}

Send Requests to External Services in the Plugin

Only HTTP requests are supported for now. You can send requests to Nacos and K8s services with service sources configured in the gateway console, and services with a static IP or DNS source. Please be noted, HTTP client in the net/http package cannot be used here. You only use the wrapped HTTP client as shown in the sample below.
In the following sample works as below:

  1. Parse service type in the config parsing stage, and generate the corresponding HTTP client.
  2. In the HTTP request header processing stage, send a service request to the configured URL.
  3. Parse response headers and get token value using the specified key.
  4. Set the token value to the headers of the original request.
package main
import (
"errors"
"net/http"
"strings"
"github.com/alibaba/higress/plugins/wasm-go/pkg/wrapper"
"github.com/tetratelabs/proxy-wasm-go-sdk/proxywasm"
"github.com/tetratelabs/proxy-wasm-go-sdk/proxywasm/types"
"github.com/tidwall/gjson"
)
func main() {
wrapper.SetCtx(
"http-call",
wrapper.ParseConfigBy(parseConfig),
wrapper.ProcessRequestHeadersBy(onHttpRequestHeaders),
)
}
type MyConfig struct {
// The client used to initiate an HTTP request
client wrapper.HttpClient
// Request URL
requestPath string
// Use this key when extracting token header from the service response and setting a header to the request. The value is configurable.
tokenHeader string
}
func parseConfig(json gjson.Result, config *MyConfig, log wrapper.Log) error {
// Get the service name with full FQDN, e.g., my-redis.dns, redis.my-ns.svc.cluster.local
serviceName := json.Get("serviceName").String()
servicePort := json.Get("servicePort").Int()
if servicePort == 0 {
config.requestPath = json.Get("requestPath").String()
if config.requestPath == "" {
return errors.New("missing requestPath in config")
}
serviceSource := json.Get("serviceSource").String()
// If serviceSource is set to "ip" or "dns", serviceName shall be specified when creating the service.
// If serviceSource is set to "nacos" or "k8s", serviceName shall be set to the original name specified when registering the service.
serviceName := json.Get("serviceName").String()
servicePort := json.Get("servicePort").Int()
if serviceName == "" || servicePort == 0 {
return errors.New("invalid service config")
}
switch serviceSource {
case "k8s":
namespace := json.Get("namespace").String()
config.client = wrapper.NewClusterClient(wrapper.K8sCluster{
ServiceName: serviceName,
Namespace: namespace,
Port: servicePort,
})
return nil
case "nacos":
namespace := json.Get("namespace").String()
config.client = wrapper.NewClusterClient(wrapper.NacosCluster{
ServiceName: serviceName,
NamespaceID: namespace,
Port: servicePort,
})
return nil
case "ip":
config.client = wrapper.NewClusterClient(wrapper.StaticIpCluster{
ServiceName: serviceName,
Port: servicePort,
})
return nil
case "dns":
domain := json.Get("domain").String()
config.client = wrapper.NewClusterClient(wrapper.DnsCluster{
ServiceName: serviceName,
Port: servicePort,
Domain: domain,
})
return nil
default:
return errors.New("unknown service source: " + serviceSource)
}
}
func onHttpRequestHeaders(ctx wrapper.HttpContext, config MyConfig, log wrapper.Log) types.Action {
// Use the Get function of the client to initiate an HTTP Get request.
// The timeout parameter is omitted here, whose default value is 500ms.
config.client.Get(config.requestPath, nil,
// A callback function which will be called asynchronously when receiving the response.
func(statusCode int, responseHeaders http.Header, responseBody []byte) {
// Process the response with a status code other than 200.
if statusCode != http.StatusOK {
log.Errorf("http call failed, status: %d", statusCode)
proxywasm.SendHttpResponse(http.StatusInternalServerError, nil,
[]byte("http call failed"), -1)
return
}
// Print out the status code and response body
log.Infof("get status: %d, response body: %s", statusCode, responseBody)
// Extract token value from the response header and set the header of the original request
token := responseHeaders.Get(config.tokenHeader)
if token != "" {
proxywasm.AddHttpRequestHeader(config.tokenHeader, token)
}
// Resume the original request processing workflow. Continue the process, so the request can be forwarded to the upstream.
proxywasm.ResumeHttpRequest()
})
// We need to wait for the callback to finish its process.
// Return Pause action here to pause the request processing workflow, which can be resumed by a ResumeHttpRequest call.
return types.ActionPause
}