The race to monetize static-site hype was over before it began — if you're a JAMStack developer, you're a Netlify customer. Surprisingly, the outcome of this unintentional vendor lock-in has been working out pretty well. JAMStack's paradigm of webhook-driven actions rewrites the narrative of static sites as dynamic entities. By either luck or foresight, Netlify is comfortably positioned to provide a home for static-sites as the only cloud host to focus solely on this model of development by providing features, like serverless functions.

Any market which finds itself dominated by a single vendor is bound to have some downsides. In Netlify's case, one such violation comes in the form of occasional poor documentation, especially regarding Golang function deployment. Netlify has dedicated precisely one page of documentation dedicated to writing serverless functions in Go with zero mention of how to actually deploy said functions. Compare this to Netlify's commitment to JavaScript function development, which includes a dedicated CLI, a build plugin, and documentation that covers useful details.

This tutorial assumes you have a basic understanding of Golang and GatsbyJS —this post is not a tutorial about learning Go from scratch. We will cover:

  • The basics of Go development in the context of Lambda functions.
  • Deploying Go functions as part of a GatsbyJS site hosted on Netlify.
  • Interacting with and monitoring deployed Go functions.

Creating a Go Serverless Function

We'll start by creating a new Go project on our GOPATH as we normally would:

$ mkdir ~/go/src/github.com/toddbirchard/netlify-serverless-tutorial
$ cd ~/go/src/github.com/toddbirchard/netlify-serverless-tutorial
Create a directory to store your function source.

Next, we initialize our module by running go mod init in the current folder, which should produce the following outcome:

module github.com/toddbirchard/netlify-serverless-tutorial
  
go 1.14
go.mod

Naming your Go module to match the URL of a Github repository is a common practice you should already abide by. Still, it's especially important to do so for our purposes. Netlify creates serverless functions by taking the Go source code we give and building executable binaries. To do so, Netlify needs to be aware of the dependencies our project might have. Instead of uploading our Go source with all its dependencies, Netlify intelligently looks to the dependencies found in our module's Github repository and pulls them itself.

Get started by pasting the minimum boilerplate into a new file called main.go:

package main

import (
    "github.com/aws/aws-lambda-go/events"
    "github.com/aws/aws-lambda-go/lambda"
)

func Handler(request events.APIGatewayProxyRequest) (*events.APIGatewayProxyResponse, error) {
    return &events.APIGatewayProxyResponse{
        StatusCode: 200,
        Body:       "Hello World!",
    }, nil
}

func main() {
    // Initiate AWS Lambda handler
    lambda.Start(Handler)
}
main.go

Oh snap, we've imported an external library! The aws-lambda-go library is essential for our function to work. Get and install the library by running go get in your project directory. This will update your go.mod file accordingly:

module github.com/toddbirchard/netlify-serverless-tutorial

go 1.14

require github.com/aws/aws-lambda-go v1.18.0
go.mod

Working with Request Data

Netlify Lambda functions take the form of HTTP endpoints, so odds are you're building something that takes the input of a request (whether it be a request's  params or body) to produce a response. We can access information about an incoming request via aws-lambda-go's APIGatewayProxyRequest struct: Lambda's default parameter passed to our handler.

Let's see this in action by grabbing a query string parameter from an incoming request and outputting the result. We can easily modify our original handler to check an incoming request for a parameter called name (i.e., example.com/function?name=todd):

...

func Handler(request events.APIGatewayProxyRequest) (*events.APIGatewayProxyResponse, error) {
    name := request.QueryStringParameters["name"]
    response := fmt.Sprintf("Hello %s!", name)

    return &events.APIGatewayProxyResponse{
        StatusCode: 200,
        Headers:    map[string]string{"Content-Type": "text/html; charset=UTF-8"},
        Body:       response,
    }, nil
}

...
main.go

APIGatewayProxyRequest has a variable called QueryStringParameters, which is a key/value map of query sting names and their values. The above attempts to extract the value of a query string called name to create a cheerful response for the user. If an incoming request were not to contain a query string called name, the value would return nil.

We could extract a lot more from a APIGatewayProxyRequest struct if we so chose:

  • Path: URL requested by the user which generated the request.
  • HTTPMethod: GET, POST, PUT, etc.
  • Headers & MultiValueHeaders: HTTP headers sent by the user's request.
  • QueryStringParameters & QueryStringParameters: Query strings with single or multiple values.
  • Body: Data (such as JSON) sent in the body of the request.

The output of a Lambda function is created by returning APIGatewayProxyResponse. Our response returns a simple 200 status code, along with a text response reading "Hello {name}!". We specify the content type of the response via our response's Headers variable, which accepts a map of headers such as Content-Type.

Testing our Function

I promised to spend minimal time on the actual Golang aspect of this post, but writing unit tests for Lambda functions is particularly tricky as it involves forming fake requests and evaluating their responses. aws-lambda-go has a recommended syntax for writing multiple tests against a single handler which I found quite useful:

package main_test

import (
    "github.com/aws/aws-lambda-go/events"
    "github.com/stretchr/testify/assert"
    main "github.com/toddbirchard/netlify-serverless-tutorial"
    "log"
    "testing"
)

func TestHandler(t *testing.T) {
    tests := []struct {
        request events.APIGatewayProxyRequest
        expect  string
        err     error
    } {
        {
            // Test name has value
            request: events.APIGatewayProxyRequest{QueryStringParameters: map[string]string{"name": "Paul"}},
            expect:  "Hello Paul!",
            err:     nil,
        },
        {
            // Test name is null
            request: events.APIGatewayProxyRequest{},
            expect:  "Hello !",
            err:     nil,
        },
    }

    for i, test := range tests {
        response, err := main.Handler(test.request)
        assert.IsType(t, test.err, err)
        assert.Equal(t, test.expect, response.Body)
        log.Printf("Test %d: %s", i, response.Body)
    }
}
main_test.go

TestHandler creates two hypothetical situations (aka tests) to run against our Handler function. The first test passes a query string parameter "Paul", while the second passes none at all. These scenarios are then evaluated in a loop, which outputs the following when tested with go test:

2020/08/01 03:17:47 Test 0: Hello Paul!
2020/08/01 03:17:47 Test 1: Hello !
PASS
ok      github.com/toddbirchard/netlify-serverless-tutorial     0.139s
main_test.go output

The validity of each test is validated via assertions belonging to the testify module, which I recommend for unit tests like these. Be sure to go get this module if you chose to include it:

module github.com/toddbirchard/netlify-serverless-tutorial

go 1.14

require (
    github.com/aws/aws-lambda-go v1.18.0
    github.com/stretchr/testify v1.4.0
)
go.mod

We now have a Lambda function which is complete enough to deploy.... but how do we go about that?

Serverless Functions in GatsbyJS

The structure of a minimal GatsbyJS project looks something like this:

/my-gatsby-project
├── /src
├── /public
├── /static
├── gatsby-browser.js
├── gatsby-config.js
├── gatsby-node.js
└── package.json
GatsbyJS project structure.

All of the files and directories above are standard Gatsby stuff doing Gatsby things. Things become a little more interesting when we deploy a Gatsby site with Lambda functions, as our Lambda's source code and binary is going to live side-by-side with our Gatsby code. This takes the form of two directories: a /functions directory and a /go-src directory:

/my-gatsby-project
├── /functions
|    └── helloworld
├── /go-src
|    └── /helloworld
|        └── main.go
├── /src
├── /public
├── /static
├── gatsby-browser.js
├── gatsby-config.js
├── gatsby-node.js
└── package.json
GatsbyJS project structure.

/go-src is where we dump the source code of our Golang functions (.go files). We create subdirectories in go-src for each Lambda function we want to deploy. Go source code in each subdirectory will be compiled to a binary sharing the name of the directory in the /functions folder. In the above example, the /helloworld subdirectory compiles to a binary in functions/helloworld, which ultimately dictates the eventual URL of our endpoint as well, as such:

https://hackersandslackers.com/.netlify/functions/helloworld
URL of a deployed Golang Lambda function.

If you're wondering how the source of main.go got into our new /go-src/helloworld destination, the underwhelming answer is a simple copy-and-paste. No tricks there.

Building Netlify Lambda Binaries in GatsbyJS

Let's recap where we're at:

  • GatsbyJS project: Check ✅
  • Lambda source code: Check ✅
  • Binaries built from source upon project builds: Not check 🚫

When we deploy to Netlify, we want our project to find the source in /go-src and build the resulting binaries to the /functions directory. Netlify looks for a build command in netlify.toml, which would typically be gatsby build under normal circumstances. Since we're now building our site and functions, we're going to recruit the help of a Makefile to enable a more involved build process.

Make sure you have a netlify.toml file in your GatsbyJS project root where we can specify the build command of our Makefile, as well as define our functions folder:

[build]
  base = "/"
  command = "make build"
  publish = "/public"
  functions = "/functions"

[build.environment]
  GO_VERSION = "1.14.5"
netlify.toml

We've changed what you most likely had as command = "gatsby build" to command = "make build", which looks for a Makefile containing a build command to execute. Let's create that now.

Making a Makefile

If you're new to Makefiles, they're a standard fixture in projects that allow us to define one-liners that kick off a series of events, such as building a project. Our Makefile is going to look like this:

build:
   npm run-script build
   mkdir -p functions
   GOOS=linux
   GOARCH=amd64
   GO111MODULE=on
   GOBIN=${PWD}/functions go get ./...
   GOBIN=${PWD}/functions go install ./...
Makefile

Hopefully, you find this string of commands to be confusing as shit as I did (you might be overqualified for reading this post otherwise). We'll break down this build command line-by-line:

  • npm run-script build: This command runs a script in package.json labeled as "build," which is likely running gatsby build. This step is building your Gatsby site as you normally would for production, so nothing special there.
  • mkdir -p functions: Creates a directory called functions in your project's root directory, in case it doesn't exist. Since I don't recommend uploading your compiled binaries to Github (Netlify will rebuild them regardless), there's a good chance this folder won't exist upon committing. As a result, we need this command as a way to create a fresh functions folder on our Netlify server.
  • GOOS=linux & GOARCH=amd64: Golang's GOOS and GOARCH environment variables tell Go which target operating system (Linux) and architecture (AMD 64-bit) our binaries are intended to be built for. The values we're passing here align with the OS and architecture of Netlify's server, thus we're ensuring Go is targeting the correct infrastructure to compile binaries for.
  • GO111MODULE=on: Things get a bit interesting here as Go source we're building in /go-src is not actually on our GOPATH, nor will it be upon deployment to Netlify. GO111MODULE is a workaround for building Go projects outside of the designated GOPATH; this allows us to get dependencies of functions living in our Gatsby project and build the resulting binaries without worrying about being shackled to our (or Netlify's) GOPATH.
  • GOBIN=${PWD}/functions go get ./...: Lastly, we need to get and install all of our function's dependencies to build them into our final binary. This line is effectively looking for all go modules to build into binaries and builds them in the /functions folder.

Give it a Whirl

We're ready to test our function locally! Run make build in your Gatsby root folder to make sure things run smoothly. If you're successful, you should see an output like this towards the end of your build:

info Done building in 63.568185195 sec
mkdir -p functions
GOOS=linux
GOARCH=amd64
GO111MODULE=on
GOBIN=/Users/toddbirchard/projects/stockholm/functions go get ./...
GOBIN=/Users/toddbirchard/projects/stockholm/functions go install ./...
Output of make build

You should now see Go binaries in your local /functions directory! Add your /functions directory to your .gitignore file and commit that masterpiece.

See it in Action

To demonstrate a GatsbyJS site utilizing a Netlify function, I set up a demo for your pleasure here:

https://serverless-golang-tutorial.netlify.app

This is a simple GatsbyJS site that takes the output of our helloworld function and writes it to the page via some very simple frontend JavaScript. I busted some ass to put together a somewhat respectable demo for you because copying is way easier than reading. Don't say I've never done anything for you:

hackersandslackers/netlify-functions-tutorial
Write and deploy Golang Lambda Functions to your GatsbyJS site on Netlify. - hackersandslackers/netlify-functions-tutorial
GatsbyJS site with Go function.

Why is this So Convoluted?

Compared to the workflow of deploying JavaScript functions on Netlify, Golang is an undocumented and seemingly half-supported nightmare. Until just this week, inspecting your Go Lambda functions on Netlify's UI would append a .js file extension to every function by default with the presumed assumption that everybody is picking JS over Go:

Netlify doesn't care about Golang™

This is generally in line with Netlify's actions of half-assing their Go documentation, and even releasing a CLI to assist in function development called netlify-lambda, which only supports JavaScript.

Other than feeling like a second-class citizen, these gaffs are more amusing than they are harmful. Once your functions are deployed, they work as you'd expect. All is well that ends well.

If you're looking for more resources on Go Lambda development in general, AWS has a Github repo of examples that might prove useful. Aside from that and this tutorial, I think you have everything you need 😉. Until next time.