Building a Fast Serverless API with Lambda Functions in Go
AWS has deprecated the Go runtime for its Lambda service. The good news is that there are serious performance benefits associated with using a custom runtime based on Amazon Linux 2, especially when it comes to cold starts.
I typically write small helper and API functions for personal projects in Node.js and Python. Given the recent improvements and my appreciation for Go’s elegance, I see little reason not to write my next functions in Go. AWS even provides a lib for that. So, here we Go (pun intended).
One of my pet projects is a nutrition app that works with OpenAI APIs to estimate the nutritional content of meals by text description and picture. I migrated the functions of that project to Go. I invoke them via Lambda function URLs in order to provide a simple API. Here is the boilerplate code I wrote for this. The example function receives a POST request and returns the “message” field from the POST body ({ “message”: “Hello World!” }) in its response. Replace this with your own logic.
package main
import (
"context"
"encoding/json"
"fmt"
"github.com/aws/aws-lambda-go/events"
"github.com/aws/aws-lambda-go/lambda"
)
func request(ctx context.Context, event events.LambdaFunctionURLRequest) (events.LambdaFunctionURLResponse, error) {
var request map[string]string
err := json.Unmarshal([]byte(event.Body), &request)
if err != nil {
return events.LambdaFunctionURLResponse{}, fmt.Errorf("error parsing request body: %v", err)
}
message, exists := request["message"]
if !exists {
return events.LambdaFunctionURLResponse{}, fmt.Errorf("message not found in request")
}
return events.LambdaFunctionURLResponse{StatusCode: 200, Body: "echo: " + message}, nil
}
func main() {
lambda.Start(request)
}
My project includes multiple Lambda functions. I have organized these using the following directory structure: each subdirectory contains the code for one Lambda function.
The custom runtime requires that the compiled binary be named “bootstrap” and placed inside a ZIP file. To execute tests, compile the binary for the Amazon Linux 2 platform, and package the ZIP file, I wrote a small Makefile:
# Variables
GOOS=linux
GOARCH=amd64
DIRS=nutritional-value-estimator nutritional-value-estimator-image
BINARY_NAME=bootstrap
ZIP_NAME=lambda-handler.zip
# Default target
all: test $(addsuffix /out/$(ZIP_NAME),$(DIRS))
# Rule for running tests
test:
@echo "Running tests..."
@go test -v ./...
# Rule for building Go binaries
%/out/$(BINARY_NAME): %/main.go
mkdir -p $(dir $@)
GOOS=$(GOOS) GOARCH=$(GOARCH) go build -o $@ $<
# Rule for zipping the binaries
%/out/$(ZIP_NAME): %/out/$(BINARY_NAME)
cd $(dir $<) && zip $(ZIP_NAME) $(BINARY_NAME)
.PHONY: all test
The makefile produces a file “lambda-handler.zip” in the out directory of each subdirectory, ready to be uploaded to AWS Lambda, or deployed via IaC. This results in an ultra-fast Lambda function with a cold start time of 65ms and a subsequent runtime of just 1ms.
Comments