Serverless with Go and Azure Functions custom handlers

Tutorials

/

Posted on

September 29, 2020

I love Go and I like Azure Functions, but at the time of this writing there is no native language handler for Go. Do not despair! There is an alternative if a way to run serverless Go in Azure Functions is desired: Custom handlers.

Custom handlers makes it possible to run any language that supports a way to serve and respond to HTTP events in an Azure Function App. This article will focus on how to get started with custom handlers and Go.

Functions and Go Gopher

The tools that will be used in this article:

So is there any point in doing all the extra steps needed to get going with Go and Function Apps? I would say there is. Even if the old saying "Just because you can doesn't mean that you should" comes to mind.What are the reasons to go with Go and custom handlers?

  • The development team/developer is more experienced in using and leveraging Go than the supported languages
  • The development team/developer has libraries and other dependencies that they are used to/need for the project (included with this is taking tooling, development and standards into consideration)
  • For more complex solutions the dependency tree for a Node.js project can grow quite large (and contain many files), this doesn't happen with the single executable produced by Go
  • Go is LIT🔥

So without further ado, let's get cookin'!

The Function App parts

Getting started with Functions is a breeze when using the Function Core Tools. To start things of, create a folder that should contains the function and initiate it as an Azure Function App (the directory name should be the name of the project, in this case gofunc):

Modify host.json to resemble this:

Note: For production, remove the extensionBundle part and install the required extensions into the project with: func extensions install.

Note: If developing for Linux based Function Apps omit .exe in "defaultExecuteablePath": "handler.exe".

Edit .gitignore (we don't want to add executables to our source control):

Next step is creating an HTTP triggered function definition (triggers and bindings):

This creates the folder IncomingHTTP containing functions.json.

Edit IncomingHTTP/functions.json and update "methods": [ "get", "post" ] to "methods": [ "post" ].

Before proceeding an explanation on how data is passed from the function host to the custom handler is in order.

As pointed out earlier in the article, custom handlers supports any language that supports serving and responding to HTTP events. The way this is done is that the function host will send/receive HTTP payloads to/from the custom handler, in this case an HTTP server.

This means that our handlers within our HTTP server will have to process the incoming data according to the expected format.

The Go parts

Let's get down to business and write some Go!

This article will seperate the server from the handlers (functions in the context of a function app). Let's begin with the function(s):

Carrying on, let's create the request and response types for bindings and triggers:

Note: These structs can be replaced with the use of maps and empty interfaces within our handlers to decode the JSON to needed data types and back, but for clarity and since we know the expected structure of the incoming data, a more defined structure was chosen instead.

The reasoning behind using anonymous nested structs in both HTTPRequest and HTTPResponse is that it's only the Body(which is served as an escaped JSON string) and StatusCode, that we care about and need to access to, to be able to use and manipulate the incoming data.

Additionally the Payload struct is used to decode the actual body of the incoming request. When writing more advanced functions, this struct should match up with the payload you expect to handle within your function.

Continuing with the functions:

Note: When function host receives an error from the custom handler it will always intepret and return 500 - Internal Server Error. Because of that the StatusCode part of HTTPResponse is used for non 500 errors. Providing the 400 (BadRequest) in StatusCode makes the function host intepret and return the correct error.

Finally to serve the function(s):

Note: Notice how the route for the functions has the same name as the directory created earlier containing the function.json file. The same pattern applies when creating additional functions.

Publishing the application to a Function App

Provision the proper resources for the Function App. A deployment ARM template can be found here.

Create the file .funcignore with the following content (these are files that shouldn't be published):

If you didn't specify the worker runtime as Custom during the provisioning of your Function App a setting needs to be added like so:

Compile the Go application and publish the project:

Conclusions

So here we are, we've done it. We have created am application that runs Go with Function Apps with the help of custom handlers. Isn't that neat? Yes and no.

I've performed some simple (and somewhat naïve) benchmarks and compared this function app with ones created with Node.js and C# (.NET) target runtimes. The results where overall even between them all, Go and Node.js taking the lead and the slower one being C#.

These were as said simple testing so the results doesn't say that much. I still find it quite interesting that the Go Functions with custom handlers kept up with the others when taking the additional layers (data decode/encode and the HTTP server) into consideration.

The big drawback is the increased development time that is needed due to the nature of custom handlers, and there is not as much boilerplate function code as for C#, and not being so simplistic as the function code for Node.js.

For my part I will explore the area further and write more Function App projects with custom handlers and Go.

The whole project with all code and a deployment template can be found in this GitHub repository. Follow us on LinkedIn if you liked this post. Connect if you want to discuss all things Azure, Serverless and Go.

Sources

Written by

Karl Wallenius