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:
Next, we initialize our module by running go mod init
in the current folder, which should produce the following outcome:
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 themselves.
Get started by pasting the minimum boilerplate into a new file called 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:
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
):
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 who 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:
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
:
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:
We now have a Lambda function, but it is not yet "complete" enough to deploy. How do we go about that?
Serverless Functions in GatsbyJS
The structure of a minimal GatsbyJS project looks something like this:
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:
/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:
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:
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:
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 runninggatsby 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 toget
andinstall
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:
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:
This is a simple GatsbyJS site that takes the output of our helloworld function and writes it to the page via some very simple front-end 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:
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:
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.