AWS Lambda in GoLang — The Ultimate Guide

12 min read
AWS Lambda in GoLang — The Ultimate Guide

AWS Lambda has been leading the way in serverless and cloud computing in recent years. So it comes as no surprise that major companies are making the switch to serverless architecture to shorten the time it takes in bringing their products to market and decreased operational costs,

AWS Lambda is a service or FaaS (Function as a Service) provided by Amazon Web Services. It supports a wide array of potential triggers, including incoming HTTP requests, messages from a queue, customer emails, changes to database records, user authentication, messages coming to web sockets, client device synchronization, and much more. One of its best functions is to allow users to focus on the core product and business logic instead of managing the operating system access control, etc.

Then you have Go or Golang. It is a computer programming language whose development began in 2007 at Google, and it was introduced to the public in 2009.

Go is a language loosely based on the syntax of the C programming language, it is a language that is statically typed and contains a garbage collector and memory safety is already a core part of the structure. Go eschews many features of other modern languages, such as method and operator overloading, pointer arithmetic, and type inheritance.

Go is not a free-form language: its conventions specify many formatting details, including how indentation and spaces are to be used. The language requires that none of its declared variables or imported libraries are unused, and all return statements are compulsory.

Go employs "type inference" in variable declarations: the variable type, rather than being an explicit part of the declaration statement, is inferred by the value type itself.

Also, Golang is a compiled language, which means that once you compile it for a certain environment it contains all of the necessary libraries it needs to function. There is no need to upload and install any external libraries with your binary file. Lambda functions are designed to be self-contained, small, and fast, which goes well with the ability to upload a single self-contained file for the Lambda to execute.

So it comes as no surprise to see the marriage of Golang and AWS Lambda in certain workloads.

Getting Started with AWS Lambda and Go

Being that Golang and Lambda have such high compatibility, developing codes that blend the two together isn't so hard.

You start off by creating an HTTP triggered AWS Lambda function that responds with JSON. We’ll start by building a REST application which manages users, with the application you will be able to create users, get a single user, get a list of users, update a single user or delete a user.

Before we can start, there are a few things we are going to need. First is an AWS account. If you don’t yet have one, you can easily sign up for a free one on the AWS website. The AWS free tier includes one million invocations every month and enough credit to run a single 128MB function continuously, all at no charge. We will be building using AWS CLI.

Next is to make sure we have Golang installed, you can either download an installer from the official website or use your favourite package manager to install it.

With that, we have both Golang and AWS Lambda ready to go.

Golang AWS Lambda Function Example

For this example, we will be using one main function aws-lambda-go-lang

Now that you have everything you need, let’s create a new project using Go modules for the function. In a new, empty directory, initialize the Go module.

Initialize go mod project

go mod init aws-lambda-in-go-lang

Directory structure:

├── README.md
├── build
├── cmd
│   └── main.go
├── go.mod
├── go.sum
└── pkg
    ├── handlers
    │   ├── api_response.go
    │   └── handlers.go
    └── user
        ├── is_email_valid.go
        └── user.go

Next, we proceed to create a main.go file that implements the handler function and starts the process:

cmd/main.go - in the main func we have the lambda invocation, and in the handler func, there is a simple router that triggers an individual handler function for each of REST HTTP methods.

  package main
  import (
    "aws-lambda-in-go-lang/pkg/handlers"
    "os"
    "github.com/aws/aws-lambda-go/events"
    "github.com/aws/aws-lambda-go/lambda"
    "github.com/aws/aws-sdk-go/aws"
    "github.com/aws/aws-sdk-go/aws/session"
    "github.com/aws/aws-sdk-go/service/dynamodb"
    "github.com/aws/aws-sdk-go/service/dynamodb/dynamodbiface"
  )

  var (
    dynaClient dynamodbiface.DynamoDBAPI
  )

  func main() {
    region := os.Getenv("AWS_REGION")
    awsSession, err := session.NewSession(&aws.Config{
        Region: aws.String(region)},
    )
    if err != nil {
        return
    }
    dynaClient = dynamodb.New(awsSession)
    lambda.Start(handler)
  }

  const tableName = "LambdaInGoUser"

  func handler(req events.APIGatewayProxyRequest) (*events.APIGatewayProxyResponse, error) {
    switch req.HTTPMethod {
    case "GET":
        return handlers.GetUser(req, tableName, dynaClient)
    case "POST":
        return handlers.CreateUser(req, tableName, dynaClient)
    case "PUT":
        return handlers.UpdateUser(req, tableName, dynaClient)
    case "DELETE":
        return handlers.DeleteUser(req, tableName, dynaClient)
    default:
        return handlers.UnhandledMethod()
    }
  }

If we dissect this piece of coding we can infer the following simple steps that made this all work.

We create the main function which initializes the AWS session and then triggers Lambda handler function.

Next, it creates a request handler function which as a parameter takes APIGatewayProxyRequest and returns the APIGatewayProxyResponse which contains as a body JSON payload.

pkg/handlers.go - contains functions which handle each of the REST actions, and return a proper response, if there is an error, they will show a message to the end-user that something went wrong.

  1. GetUser: handle both User Detail and User List response
  2. CreateUser: handle creating User
  3. UpdateUser: handle updating User
  4. DeleteUser: handle removing User
    package handlers

    import (
      "aws-lambda-in-go-lang/pkg/user"
      "net/http"

      "github.com/aws/aws-lambda-go/events"
      "github.com/aws/aws-sdk-go/aws"
      "github.com/aws/aws-sdk-go/service/dynamodb/dynamodbiface"
    )

    var ErrorMethodNotAllowed = "method Not allowed"

    type ErrorBody struct {
      ErrorMsg *string `json:"error,omitempty"`
    }

    func GetUser(req events.APIGatewayProxyRequest, tableName string, dynaClient dynamodbiface.DynamoDBAPI) (
      *events.APIGatewayProxyResponse,
      error,
    ) {
      email := req.QueryStringParameters["email"]
      if len(email) > 0 {
          // Get single user
          result, err := user.FetchUser(email, tableName, dynaClient)
          if err != nil {
              return apiResponse(http.StatusBadRequest, ErrorBody{aws.String(err.Error())})
          }

          return apiResponse(http.StatusOK, result)
      }

      // Get list of users
      result, err := user.FetchUsers(tableName, dynaClient)
      if err != nil {
          return apiResponse(http.StatusBadRequest, ErrorBody{
              aws.String(err.Error()),
          })
      }
      return apiResponse(http.StatusOK, result)
    }

    func CreateUser(req events.APIGatewayProxyRequest, tableName string, dynaClient dynamodbiface.DynamoDBAPI) (
      *events.APIGatewayProxyResponse,
      error,
    ) {
      result, err := user.CreateUser(req, tableName, dynaClient)
      if err != nil {
          return apiResponse(http.StatusBadRequest, ErrorBody{
              aws.String(err.Error()),
          })
      }
      return apiResponse(http.StatusCreated, result)
    }

    func UpdateUser(req events.APIGatewayProxyRequest, tableName string, dynaClient dynamodbiface.DynamoDBAPI) (
      *events.APIGatewayProxyResponse,
      error,
    ) {
      result, err := user.UpdateUser(req, tableName, dynaClient)
      if err != nil {
          return apiResponse(http.StatusBadRequest, ErrorBody{
              aws.String(err.Error()),
          })
      }
      return apiResponse(http.StatusOK, result)
    }

    func DeleteUser(req events.APIGatewayProxyRequest, tableName string, dynaClient dynamodbiface.DynamoDBAPI) (
      *events.APIGatewayProxyResponse,
      error,
    ) {
      err := user.DeleteUser(req, tableName, dynaClient)
      if err != nil {
          return apiResponse(http.StatusBadRequest, ErrorBody{
              aws.String(err.Error()),
          })
      }
      return apiResponse(http.StatusOK, nil)
    }

    func UnhandledMethod() (*events.APIGatewayProxyResponse, error) {
      return apiResponse(http.StatusMethodNotAllowed, ErrorMethodNotAllowed)
    }

It’s worth taking some time to understand how the handler function works.

When calling Lambda functions using the AWS SDK, the structure of the request and response payload is up to the developer. For AWS Lambda functions invoked by AWS services, the data structure depends on the invoking service. Amazon API Gateway is the service that triggers Lambda functions in response to HTTP calls. For API Gateway, this means the request is always of type APIGatewayProxyRequest and the response will always be of type APIGatewayProxyResponse.

The AWS Lambda for Go library contains the data definitions for each AWS service that can invoke Lambda functions

pkg/api_response.go - contains a function which takes status and body parameters and creates valid APIGatewayProxyResponse

    package handlers

    import (
      "encoding/json"

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

    func apiResponse(status int, body interface{}) (*events.APIGatewayProxyResponse, error) {
      resp := events.APIGatewayProxyResponse{Headers: map[string]string{"Content-Type": "application/json"}}
      resp.StatusCode = status

      stringBody, _ := json.Marshal(body)
      resp.Body = string(stringBody)
      return &resp, nil
    }

pkg/user/user.go - this file contains User struct and all functions which handle all CRUD operations on a DynamoDB.

There are 5 methods:

  1. FetchUser - to get single User by email address
  2. FetchUsers - to get all available Users 3, CreateUser - to create User
  3. UpdateUser - to update User
  4. DeleteUser - to delete User
  package user

  import (
    "encoding/json"
    "errors"

    "aws-lambda-in-go-lang/pkg/validators"

    "github.com/aws/aws-lambda-go/events"
    "github.com/aws/aws-sdk-go/aws"
    "github.com/aws/aws-sdk-go/service/dynamodb"
    "github.com/aws/aws-sdk-go/service/dynamodb/dynamodbattribute"
    "github.com/aws/aws-sdk-go/service/dynamodb/dynamodbiface"
  )

  var (
    ErrorFailedToUnmarshalRecord = "failed to unmarshal record"
    ErrorFailedToFetchRecord     = "failed to fetch record"
    ErrorInvalidUserData         = "invalid user data"
    ErrorInvalidEmail            = "invalid email"
    ErrorCouldNotMarshalItem     = "could not marshal item"
    ErrorCouldNotDeleteItem      = "could not delete item"
    ErrorCouldNotDynamoPutItem   = "could not dynamo put item error"
    ErrorUserAlreadyExists       = "user.User already exists"
    ErrorUserDoesNotExists       = "user.User does not exist"
  )

  type User struct {
    Email     string `json:"email"`
    FirstName string `json:"firstName"`
    LastName  string `json:"lastName"`
  }

  func FetchUser(email, tableName string, dynaClient dynamodbiface.DynamoDBAPI) (*User, error) {
    input := &dynamodb.GetItemInput{
        Key: map[string]*dynamodb.AttributeValue{
            "email": {
                S: aws.String(email),
            },
        },
        TableName: aws.String(tableName),
    }

    result, err := dynaClient.GetItem(input)
    if err != nil {
        return nil, errors.New(ErrorFailedToFetchRecord)

    }

    item := new(User)
    err = dynamodbattribute.UnmarshalMap(result.Item, item)
    if err != nil {
        return nil, errors.New(ErrorFailedToUnmarshalRecord)
    }
    return item, nil
  }

  func FetchUsers(tableName string, dynaClient dynamodbiface.DynamoDBAPI) (*[]User, error) {
    input := &dynamodb.ScanInput{
        TableName: aws.String(tableName),
    }
    result, err := dynaClient.Scan(input)
    if err != nil {
        return nil, errors.New(ErrorFailedToFetchRecord)
    }
    item := new([]User)
    err = dynamodbattribute.UnmarshalListOfMaps(result.Items, item)
    return item, nil
  }

  func CreateUser(req events.APIGatewayProxyRequest, tableName string, dynaClient dynamodbiface.DynamoDBAPI) (
    *User,
    error,
  ) {
    var u User
    if err := json.Unmarshal([]byte(req.Body), &u); err != nil {
        return nil, errors.New(ErrorInvalidUserData)
    }
    if !validators.IsEmailValid(u.Email) {
        return nil, errors.New(ErrorInvalidEmail)
    }
    // Check if user exists
    currentUser, _ := FetchUser(u.Email, tableName, dynaClient)
    if currentUser != nil && len(currentUser.Email) != 0 {
        return nil, errors.New(ErrorUserAlreadyExists)
    }
    // Save user

    av, err := dynamodbattribute.MarshalMap(u)
    if err != nil {
        return nil, errors.New(ErrorCouldNotMarshalItem)
    }

    input := &dynamodb.PutItemInput{
        Item:      av,
        TableName: aws.String(tableName),
    }

    _, err = dynaClient.PutItem(input)
    if err != nil {
        return nil, errors.New(ErrorCouldNotDynamoPutItem)
    }
    return &u, nil
  }

  func UpdateUser(req events.APIGatewayProxyRequest, tableName string, dynaClient dynamodbiface.DynamoDBAPI) (
    *User,
    error,
  ) {
    var u User
    if err := json.Unmarshal([]byte(req.Body), &u); err != nil {
        return nil, errors.New(ErrorInvalidEmail)
    }

    // Check if user exists
    currentUser, _ := FetchUser(u.Email, tableName, dynaClient)
    if currentUser != nil && len(currentUser.Email) == 0 {
        return nil, errors.New(ErrorUserDoesNotExists)
    }

    // Save user
    av, err := dynamodbattribute.MarshalMap(u)
    if err != nil {
        return nil, errors.New(ErrorCouldNotMarshalItem)
    }

    input := &dynamodb.PutItemInput{
        Item:      av,
        TableName: aws.String(tableName),
    }

    _, err = dynaClient.PutItem(input)
    if err != nil {
        return nil, errors.New(ErrorCouldNotDynamoPutItem)
    }
    return &u, nil
  }

  func DeleteUser(req events.APIGatewayProxyRequest, tableName string, dynaClient dynamodbiface.DynamoDBAPI) error {
    email := req.QueryStringParameters["email"]
    input := &dynamodb.DeleteItemInput{
        Key: map[string]*dynamodb.AttributeValue{
            "email": {
                S: aws.String(email),
            },
        },
        TableName: aws.String(tableName),
    }
    _, err := dynaClient.DeleteItem(input)
    if err != nil {
        return errors.New(ErrorCouldNotDeleteItem)
    }

    return nil
  }

pkg/validators/is_email_valid.go - contains function which check if the given email is valid

  package validators

  import "regexp"

  func IsEmailValid(email string) bool {
    var rxEmail = regexp.MustCompile("^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]{1,64}@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$")

    if len(email) < 3 || len(email) > 254 || !rxEmail.MatchString(email) {
        return false
    }
    return true
  }

Test Golang AWS Lambdas

Testing out things isn't so complicated.

What we need to do is fire things up.

To Install dependencies

go get -v all

Build project

GOOS=linux go build -o build/main cmd/main.go

We need to Zip binary to be able to upload to AWS lambda

zip -jrm build/main.zip build/main

How to Deploy AWS Lambda on AWS

But before we can begin running Lambda within AWS you have to set up the environment. The steps involved in deploying your application are not very complicated.

Remember you must already have an AWS account before you start and you can start off with their free tier to get your feet wet. So the first thing you would want to do to go to the service finder and select Lambda

Next in Lambda page, click Create Function to create Lambda function

In the next screen type:

  • Function name: lambda-in-go-lang
  • Runtime: Go
  • Execution role: Create a new role from aws policy templates
  • Role Name: lambda-in-go-lang-executor - and choose Simple microservice permission

It will take a few seconds to create our Lambda function. In the Lambda function screen change:

  • handler to: main
  • Upload created previously main.zip file with our packed binaries

OK, now the next would be DynamoDB, so in services find DynamoDB In the DynamoDB page, click Create table

On the next screen type:

  • Table name: LambdaInGoUser
  • Primary key: email And click Create

The last Step would be create an API Webgate Once again from Services find API Webgate

In REST API section click Build button

On the next screen select:

  • Chose the protocol: REST
  • Create a new API: New API
  • And API name: lambda-in-go-lang-api

And click Create API

On the next screen from actions, screen select Create Method and Choose ANY

Select:

  • Integration type: Lambda Function
  • Use Lambda Proxy integration: check
  • Lambda Function: lambda-in-go-lang
  • Use Default Timeout: check

When you try to save the modal window will pop up, click OK

Next screen should look like this:

Now we need to Deploy our API, from Actions, drop-down menu, select Deploy API

  • Deploy stage: [New Stage]
  • Stage name: staging

And Click Deploy

After deploying API we would gain URL which is visible in a blue box

Now everything is ready for us to test.

A few simple things you might like to start with is manipulating user accounts.

Add new User:

$ curl --header "Content-Type: application/json" --request POST --data '{"email": "s.karasiewicz@softkraft.co", "firstName": "Sebastian", "lastName": "Karasiewicz"}' https://cxmyezqkte.execute-api.eu-central-1.amazonaws.com/staging

[{"email":"s.karasiewicz@softkraft.co","firstName":"Sebastian","lastName":"Karasiewicz"}]

List all users

$ curl -X GET https://cxmyezqkte.execute-api.eu-central-1.amazonaws.com/staging

[{"email":"s.karasiewicz@softkraft.co","firstName":"Sebastian","lastName":"Karasiewicz"}]

Get a single user by email

$ curl -X GET https://cxmyezqkte.execute-api.eu-central-1.amazonaws.com/staging\?email\=s.karasiewicz@softkraft.co

{"email":"s.karasiewicz@softkraft.co","firstName":"Sebastian","lastName":"Karasiewicz"}

Update User

curl --header "Content-Type: application/json" --request PUT --data '{"email": "s.karasiewicz@softkraft.co", "firstName": "Sebas", "lastName": "Karasiewicz"}' https://cxmyezqkte.execute-api.eu-central-1.amazonaws.com/staging

{"email":"s.karasiewicz@softkraft.co","firstName":"Seba","lastName":"Karasiewicz"}

Delete user

$ curl -X DELETE https://cxmyezqkte.execute-api.eu-central-1.amazonaws.com/staging\?email\=s.karasiewicz@softkraft.co