DEV Community

Cover image for Choosing Go when Building Lambdas
Benjamen Pyle for AWS Community Builders

Posted on • Originally published at binaryheap.com

Choosing Go when Building Lambdas

So you've decided to build your first or your 500th Lambda function with AWS. Congratulations! That in and of itself is a great decision that will set you up on a solid foundation for operational excellence, ease of maintenance, flexibility to extend and a whole host of other positives that come along with Serverless. Now, what language are you going to develop this new Lambda in? I've been a tremendous proponent for choosing Go when building Lambdas and I'd like to walk you through why.

Choosing a Lambda Runtime

When you begin building your first Lambda, that run-time decision has to be one of the first things you decide. Currently, AWS supports a host of different options that can be explored on the AWS Runtimes Lambda page. One of the best things about building with Lambdas is that since your scope of the problem is isolated to almost a "nano" level, you can free yourself up from being locked into just one set of technologies.

What I mean by that is that certain languages and frameworks are better suited to solve certain problems and you can explore those solutions with that set of capabilities while continuing to build other functionality in another completely different set of technologies and frameworks. And if you found out that say this Lambda is not performing up to how you expected for whatever reason, the time and energy investment should have been low enough that porting to something else isn't the end of the world. I've seen that happen several times throughout my Serverless building.

Each language and framework will have an impact on the profile of your function. Things such as:

  • Cold Starts - time for the Lambda to be deployed and run initially
  • Cost - Price per GB sec. Essentially memory allocated and time component
  • Memory - How much memory does your function need to be allocated to run well
  • Bundle size - The bigger the bundle the longer to deploy and the longer it to start

Choosing Go when Building Lambdas

First off, this is purely a list of reasons based on my experience and opinions. I've deployed Lambdas in Go, Node, .NET Core, Java, Python and Rust honestly you can have success with any language you choose. But below are the reasons that I first reach for the gopher when building my functions.

Developer Experience

I get it, this is subjective but again this is an opinion piece. For me, the built-in Go tools and the breadth of the standard library are just enough for me. So many little details are taken care of.

Testing

I don't need to pick Mocha, Jest, jUnit, nUnit or some other outside library for running my Go unit tests. It is built into the stack

package sample
import "testing"

func Test_Should_Do_Something(t *testing.T) {
    if someCondition {
        t.Fail()
    }
}
Enter fullscreen mode Exit fullscreen mode

Then being able to run go test and that's it.

Standard Library

Like it or not, the API world runs on JSON. And Go has native support with a built-in library for dealing with JSON. Its encoding package is comprehensive and all of the Marshalling and Unmarshalling of objects on and off the wire seems to work the same way

package sample
import "encoding/json"

// unmarhsall
i := SomeStruct{}
err := json.Unmarshal(bytes, &i)

// marshall
i := SomeStruct{}
out, err := json.Marshal(&i)
Enter fullscreen mode Exit fullscreen mode

This native functionality will come in handy as you parse inputs and format outputs when working with SQS, Step Functions, DynamoDB and other services in the AWS ecosystem.

Building Go

Go's build process is quick. Why does that matter when developing functions? Feedback. Sure, IDEs and VSCode are great at identifying issues but if you don't have compile-time issues and just want to start running your code, speed matters. Build and test a few dozen times a day and the savings you gain will allow you to do just a few more cycles than you could have otherwise.

This is understated in modern languages in my opinion and Go from the start set out to build a language that compiled quickly. This also plays into local development and build and deploy when using CDK or SAM if you've got say 10 or 15 functions in an API. Having that it compiles quickly before launching the API locally to test is saving you valuable time that you can be spending on code.

Code Formatting

Go fmt I could almost leave this alone by stating that. But to expound a little bit, I've seen formatting of codebases either be something that is vehemently argued or something is just left to whomever so the codebase never gets formatted. With Go fmt you have a community-supported way of formatting your code so that it always looks the same, regardless of where it comes from. Your IDE has support and the command line has a tool for it so you don't have to think about it. This is a bigger deal than you think.

Clean yet verbose syntax

Go has a familiar looking syntax. You'll notice things like {}, () in addition to all the usual looping and control constructs. Comments look like C and C++. The language can be a touch verbose at times but once you get used to it, it makes a ton of sense. Specifically when it comes to error management.
Go lacks inheritance but it makes up for it with a robust Interface design as well as favoring to leverage composition for structs which again, once you get wrapped around the patterns make for some clean and powerful code.

The ability to return multiple values. This might seem strange at first but I find it more obvious as to what the func is doing instead of building a Class that holds my return object. Take this code:

public class SomeClass {
    public String theString;
    public Integer theInteger
}

function SomeFunction() SomeClass {
    return new SomeClass() {
        theString: "ABC",
        theInteger 123
    };
}
Enter fullscreen mode Exit fullscreen mode

To me, this isn't nearly as readable as this code

func SomeFunction() (string, int) {
    return "ABC", 123
}
Enter fullscreen mode Exit fullscreen mode

It is explicit what the function returns and saves me from havening to build a separate "data carrier" to return my output. Or worse yet, would be to have some kind of "ref" that the inbound value gets changed and mutates some state that is hidden from the client.

CDK Support

If you aren't familiar with CDK, here is a good intro article to get you started.

Building infrastructure as code has so many benefits and using CDK makes that almost enjoyable. Fortunately, Go support for CDK is built right now. You can declare a function that will get built for the correct Lambda architecture by using the GoFunction construct.

new GoFunction(this, "TheFunc", {
    entry: path.join(__dirname, "some-path"),
    functionName: "func-name",
});
Enter fullscreen mode Exit fullscreen mode

Have a look at the documentation for more CDK options.

The above are just a handful of reasons I love working with Go even outside of Lambdas.

Operational Experience

Bundle Size

Let's go back to bundle size. This matters for a couple of reasons.

  1. Deployment time. The smaller the bundle the quicker to build and deploy. If your bundle is 540MB vs say 13MB that's a lot less time to push your archive up to the Lambda environment
  2. Cold Start. Again, smaller the bundle and fewer dependencies will go a long way to launching your runtime. Go launches super quickly with everything compiled into the binary so you get nice and speedy Cold Starts.

Below is an instance of a Lambda that has a few things going on including dependencies on the AWS SDK for working with DynamoDB and Parameter store yet the bundle size is only 13MB. I like that!

Bundle Size

Cold Starts

This is such a hot topic in the Serverless world. I've been deploying Lambdas for going on 6 years now in production and I can say that in the beginning, using .NET Core I would get 2 - 4 second cold starts. For async-type operations, this is not the end of the world. But if your event is attached to an API Gateway, do users want to wait that long for your first response? Probably not.

Fast forward years ahead and honestly every language has seen improvements and with the introduction of SnapStart, Java is enjoyable now for these types of use cases. I still prefer to use Go though as the below graph shows 4 days worth of Cold Start latencies on an API Endpoint. The average is just a touch over 500ms which to me is 100% acceptable as the full lifecycle with API Gateway will be under 1 second which meets my personal goal of having things return from an API Endpoint in less than that 1-second threshold.

Threshold

I attribute this to the nature of Go in that the bundle is small, the dependencies are compiled in and launching the application is just "snappy". I'll be one of the first to jump on the SnapStart train when Go support is provided but for now, I'm happy enough with this.

Runtime Complexity

This is super subjective and I debated putting it in but another thing that I love about Lambdas but also don't like is Layers. Layers are a great way to share logic across functions that have a similar purpose. But on the flip side, I find that they can hide things from me and can be a little bit tricky to test.

By using Go, I have my dependencies compiled in so I find that reusable code is shared via a package dependency that I can test locally when I'm doing my building. Again, this is not the biggest thing in the world, but it does feel a little more familiar to me when building functions.

Observability

This isn't unique to Go but here is an article I wrote on using Go with Datadog that shows how clean and simple it is to get observability baked right into your functions

Wrap Up

Coding and Deploying Lambdas is a rewarding journey of not having to focus on things that don't bring value to your customers and end users. By focusing on what matters, you can innovate and iterate on your ideas and not spend time working on things like operational infrastructure or trying to figure out how to build features in perhaps not the most ideal tech stack.

Lambdas and functions give you the smallest level of isolation so that you can focus on that problem and pick the best technology to implement a set of features. By default, I grab for Go for the reasons I showed you above. I know it's not the most popular runtime when choosing how to deploy your Lambda but I hope that this article gives you some things to think about the next time you start a new Function.

Top comments (6)

Collapse
 
iseiryu profile image
Sergey

My NestJS (NodeJS based web framework) app with AWS SDK, swagger and other stuff is only 3mb. It is a lot more difficult to build/package though.

.NET takes 150ms on the cold start. I need to check how big the package is - I've only used containers with dotnet. It has the rest of the benefits you listed in this article too.

Collapse
 
benbpyle profile image
Benjamen Pyle

That’s awesome to hear. That’s the beauty of serverless in that you’ve got a ton of choice when building your value

Collapse
 
iseiryu profile image
Sergey • Edited

Any other form of cloud hosting allows you to swap your runtime easily too. Building real time apps - when the user sits and waits for the action to complete synchronously - is a pain a butt to build and to troubleshoot with serverless. There are more cons than pros.

Thread Thread
 
benbpyle profile image
Benjamen Pyle

So I disagree with that statement on debugging. Debugging a container is no less challenging than a function or serverless components. Traceability and observability principles apply to both and make either option possible.

Local development as well is easily achieved with serverless in the build and dev process.

Additionally, the user doesn’t wait long enough on a cold start these days enough to offset the gains that are possible in other areas by going with functions. But as with anything, there is no one perfect approach. I’m a big fan of containers as well. The answer always depends. But by not embracing a single cloud provider you miss out on a ton of benefits. And unless you are an ISV my opinion is that it’s not worth missing them

Thread Thread
 
iseiryu profile image
Sergey

What is ISV?

I've been using AWS lambdas long enough to know the difference between hosting my apps on classic managed and self-managed VMs as well as containers and serverless.

Serverless is great for small things, for automation, it could be a good option for internal tools. It can also be a good option when you do not expect a lot of traffic and your budget is very tight.

It's pretty bad when it comes to building bulletproof public facing real time apps (in comparison to other forms of hosting and development). It's possible, but other options allow you to develop and troubleshoot things simpler and faster.

Thread Thread
 
benbpyle profile image
Benjamen Pyle

ISV = Independent Software Vendor.

I'm glad to have different opinions on this topic. I've been successfully building large Serverless Apps for a while now and couldn't be happier. I don't consider Lambda == Serverless so that umbrella to me includes things like SQS, SNS, Kinesis, DDB, EventBridge etc. Lambda is just a piece.

If I wanted < 5ms latency consistently, it's not my goto for things like that. So sure, a container and gRPC or another mechanism for communicating makes more sense. I might not even choose something Event-Driven in that case.

Simpler and faster ... again just opinions. Love the dialogue though and thanks for weighing in :)