Go on Compute@Edge
Compute@Edge builds Go application code using TinyGo, an alternate Go compiler with improved support for Wasm. Go is a reliable, and efficient language for building performant applications. It is a great SDK to get started with on Compute@Edge if you're familiar with traditional server-side languages.
IMPORTANT: TinyGo and Go must both be installed to compile applications written in Go.
Quick access
Documentation for the compute-sdk-go
Go module.
Filter all our code examples for those with implementations in Go.
Start a Go project from a simple but complete example application.
Project layout
If you don't yet have a working toolchain and Compute@Edge service set up, start by getting set up.
At the end of the initialization process, the current working directory will contain a file tree resembling the following:
.├── .gitignore├── README.md├── fastly.toml├── go.mod├── go.sum└── main.go
The most important file to work on is main.go
, which contains the logic you'll run on incoming requests. If you initialized your project from the default starter template, the contents of this file will match the one in the template's repo. The other files include:
- Module metadata:
go.mod
andgo.sum
describe the dependencies of your module, managed usinggo get
. - Fastly metadata: The
fastly.toml
file contains metadata required by Fastly to deploy your package to a Fastly service. It is generated by the fastly compute init command. Learn more aboutfastly.toml
.
Main interface
The most common way to start a Compute@Edge program is to define a main()
function that calls the fsthttp.ServeFunc
function using the following type signature:
9func main() {10 fsthttp.ServeFunc(func(ctx context.Context, w fsthttp.ResponseWriter, r *fsthttp.Request) {11 // ...12 })13}
The fastly/compute-sdk-go module provides the core fsthttp.Request
and fsthttp.ResponseWriter
types referenced here. The program will be invoked for each request that Fastly receives for a domain attached to your service, and it must return a response that can be served to the client.
Communicating with backend servers and the Fastly cache
A fsthttp.Request
can be forwarded to any backend defined on your service. If you specify a backend hostname as part of completing the fastly compute deploy wizard, it will be named the same as the hostname or IP address, but with .
replaced with _
(e.g., 151_101_129_57
). It's a good idea to define backend names as constants:
const BackendName = "my_backend_name"
And then reference them when you want to forward a request to a backend:
11// BackendName is the name of our service backend.12const BackendName = "httpbin"16 // This requires your service to be configured with a backend17 // named "httpbin" and pointing to "https://httpbin.org".18 resp, err := r.Send(ctx, BackendName)19 if err != nil {20 w.WriteHeader(fsthttp.StatusBadGateway)21 fmt.Fprintln(w, err.Error())22 return23 }24
25 w.Header().Reset(resp.Header)26 w.WriteHeader(resp.StatusCode)27 io.Copy(w, resp.Body)
Backends must be defined statically, since the Go SDK currently does not expose dynamic backends.
Requests forwarded to a backend will transit the Fastly cache, and the response may come from cache. Where a request doesn't find a matching result in cache, it will be sent to the origin, and the response will be cached based on the freshness rules determined from its HTTP response headers (unless overridden by fsthttp.Request.CacheOptions
).
You can also interact with the cache at a lower level using the Fastly Core Cache API. The documentation for this can be found on pkg.go.dev.
Composing requests and responses
In addition to the request passed into fsthttp.ServeFunc
and responses returned from fsthttp.Request.Send
, requests and responses can also be constructed. This is useful if you want to make an arbitrary API call that is not derived from the client request, or if you want to make a response to the client without making any backend fetch at all.
The fsthttp.NewRequest
constructor can be used to customize the request:
17 // A different URL to the incoming request.18 url := "https://httpbin.org/headers"19
20 req, err := fsthttp.NewRequest("GET", url, nil)21 if err != nil {22 log.Printf("%s: create request: %v", url, err)23 return24 }25
26 req.Header.Add("foo", "bar")27
28 resp, err := req.Send(ctx, BackendName)
The fsthttp.Response
struct can be used to customize the response:
13 h := fsthttp.NewHeader()14 h.Add("foo", "bar")15
16 resp := fsthttp.Response{17 Header: h,18 StatusCode: fsthttp.StatusOK,19 Body: io.NopCloser(strings.NewReader("Hello, world!")),20 }
Parsing and transforming responses
Requests and responses in Compute@Edge are streams, which allows large payloads to move through your service without buffering or running out of memory. Conversely, running methods such as io.ReadAll
on a fsthttp.Response.Body
will force the stream to be consumed entirely into memory. This can be appropriate where a response is known to be small or needs to be complete to be parsable.
This example will read a backend response into memory, replace every occurrence of "cat" with "dog" in the body, and then create a new body with the transformed bytes:
// Consume all response body data into memory. bs, err := io.ReadAll(resp.Body) if err != nil { w.WriteHeader(fsthttp.StatusBadGateway) fmt.Fprintln(w, err.Error()) return }
// Replace all references to "cat" with "dog". bs = bytes.ReplaceAll(bs, []byte("cat"), []byte("dog"))
// Reassign the updated body content resp.Body = io.NopCloser(bytes.NewReader(bs))
Compression
Fastly can compress and decompress content automatically, and it is often easier to use these features than to try to perform compression or decompression within your Go code. Learn more about compression with Fastly.
Using edge data
Fastly allows you to configure various forms of data stores to your services, both for dynamic configuration and for storing data at the edge. The Go SDK exposes the configstore
, kvstore
and secretstore
packages to allow access to these APIs.
All edge data resources are account-level, service-linked resources, allowing a single store to be accessed from multiple Fastly services.
Logging
The rtlog
package provides a standardized interface for sending logs to Fastly real-time logging, which can be attached to many third party logging providers. Before adding logging code to your Compute@Edge program, set up your log endpoint using the CLI, API, or web interface. Log endpoints are referenced in your code by name:
"github.com/fastly/compute-sdk-go/rtlog" fmt.Fprintln(os.Stdout, "os.Stdout can be streamed via `fastly logs tail`") fmt.Fprintln(os.Stderr, "os.Stderr can be streamed via `fastly logs tail`")
endpoint := rtlog.Open("my-logging-endpoint") fmt.Fprintln(endpoint, "Real-time logging is available via `package rtlog`")
mw := io.MultiWriter(os.Stdout, endpoint, w) fmt.Fprintln(mw, "Mix-and-match destinations with helpers like io.MultiWriter")
fmt.Fprintln(mw, "Several environment variables are defined by default...") for _, key := range []string{ "FASTLY_CACHE_GENERATION", "FASTLY_CUSTOMER_ID", "FASTLY_HOSTNAME", "FASTLY_POP", "FASTLY_REGION", "FASTLY_SERVICE_ID", "FASTLY_SERVICE_VERSION", "FASTLY_TRACE_ID", } { fmt.Fprintf(mw, "%s=%s\n", key, os.Getenv(key)) }
prefix := fmt.Sprintf("%s | %s | ", os.Getenv("FASTLY_SERVICE_VERSION"), r.RemoteAddr) logger := log.New(os.Stdout, prefix, log.LstdFlags|log.LUTC) logger.Printf("It can be useful to create a logger with request-specific metadata built in")
Using dependencies
Compute@Edge's Go support is currently provided by the TinyGo compiler, which supports the WASI (WebAssembly System Interface) system call interface used by Compute@Edge, and generates very small binaries. TinyGo's support for Reflection and other Go features is incomplete, so it doesn't support all standard Go libraries yet, but it is complete enough to be useful and comfortable.
In particular, TinyGo currently doesn't support the full net/http
package. The fsthttp
package is available in Compute@Edge and can often be used for the same use cases.
Our Fiddle tool allows the use of a subset of packages that we have tested and confirmed will work with Compute@Edge. These include packages that can fill the gaps where language features are missing:
- github.com/go-kit/log (v0.2.1)
- github.com/valyala/fastjson (v1.6.4)
- github.com/apex/log (v1.9.0)
- github.com/buger/jsonparser (v1.1.1)
- github.com/fastly/compute-sdk-go (v0.1.2)
This is a tiny fraction of the modules which will work on Compute@Edge, but these are the most commonly useful modules when building applications.
Testing and debugging
Logging is the main mechanism to debug Compute@Edge programs. Log output from live services can be monitored via live log tailing. The local test server and Fastly Fiddle display all log output automatically. See Testing & debugging for more information about choosing an environment in which to test your program.
Most common logging requirements involve HTTP requests and responses. It's important to do this in a way that doesn't affect the main program logic, since consuming a response body can only be done once. The section Parsing and transforming responses demonstrates how you can read the response body, transform the content, then reassign the updated content so it may be read again using io.NopCloser
before being written to the client.
Unit testing
You may choose to write unit tests for small, independent pieces of your Go code intended for Compute@Edge. However, Compute@Edge apps heavily depend on and interact with Fastly features and your own systems.
You can use go test -tags nofastlyhostcalls
or tinygo test -tags nofastlyhostcalls
to run unit tests for code that imports the fastly/compute-sdk-go
package, as long as they don't do any I/O using those modules. Integration tests that do I/O can be run inside a local test server using the Fastly CLI running fastly compute serve
.
Functions should ideally have a signature that makes it easy to test. Avoid accepting concrete Fastly types in favour of more primitive types or use an interface. This allows the test environment to easily define a mock implementation.
Optimizations
The TinyGo compiler provides various build options that can help optimize your compiled Wasm binary.
These options are set on the tinygo build
command using flags.
Option | Result | Trade-off |
---|---|---|
-no-debug | Reduced size of Wasm binary | No line numbers will be present in panic tracebacks |
-opt=2 | Improved runtime performance | Slightly larger Wasm binary, and less informative tracebacks |
-gc=leaking | Improved runtime performance | No automated garbage collection |
To configure these optimizations, define a custom build
field inside your project's fastly.toml:
[scripts]build = "mkdir -p bin/ && tinygo build -no-debug -opt=2 -gc=leaking -target=wasi -o bin/main.wasm . && fastly compute pack --wasm-binary bin/main.wasm"