Skip to content

Writing Lambda Functions in Go

Johnny Boursiquot edited this page Dec 6, 2021 · 4 revisions

In this section, we’ll explore a couple of our Lambda functions to give you an idea of what’s typically involved.

Let’s open up cmd/handler/main.go in our editor and focus on the main() function first:

func main() {
	lambda.Start(handler)
}

We’re making use of the AWS Lambda SDK for Go here. Its lambda package has a Start function that accepts a handler interface. Anything we use as a handler must follow these rules:

  • handler must be a function
  • handler may take between 0 and two arguments.
  • if there are two arguments, the first argument must satisfy the context.Context interface.
  • handler may return between 0 and two arguments.
  • if there are two return values, the second argument must be an error.
  • if there is one return value it must be an error.

In our case, here’s what our handler function looks like:

func handler(req *events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error) {
	ctx := context.Background()
	return shoutouts.NewHandler(esqs, ddb).Handle(ctx, req)
}

This simple function hands off the incoming request to behavior defined in a shoutouts package along with the dependencies needed to handle that request, mainly a connection to SQS and DynamoDB, a Context, and the request itself. This approach allows us to keep the main package as light as possible.

The value of type *Handler returned is defined like so:

// Handler handles incoming shoutout requests.
type Handler struct {
	sqs SQSAPI
	ddb DynamoDBQuerier
}

The SQSAPI and DynamoDBQuerier types are interfaces that make it possible for our Handler to be tested locally though mocks that satisfy those interfaces without needing to have live communication with SQS or DynamoDB.

The Handler type has a Handle method which is called to handle the incoming shoutout request.

func (h *Handler) Handle(ctx context.Context, request *events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error) {
	if !authorizedRequest(request) {
		return events.APIGatewayProxyResponse{
			StatusCode: http.StatusUnauthorized,
		}, nil
	}

	params, err := url.ParseQuery(request.Body)
	if err != nil {
		return events.APIGatewayProxyResponse{
			StatusCode: http.StatusBadRequest,
			Body:       err.Error(),
		}, nil
	}

	cmd, err := h.parseCommand(ctx, &params)
	if err != nil {
		params := url.Values{}
		params.Set("text", "help")
		cmd, _ = h.parseCommand(ctx, &params)
	}

	rsp, err := cmd.execute(ctx)
	if err != nil {
		log.Println(err)
		return events.APIGatewayProxyResponse{
			StatusCode: http.StatusInternalServerError,
			Body:       err.Error(),
		}, nil
	}

	body, err := json.Marshal(rsp)
	if err != nil {
		log.Println(err)
		return events.APIGatewayProxyResponse{
			StatusCode: http.StatusInternalServerError,
		}, nil
	}

	headers := map[string]string{"Content-Type": "application/json"}
	return events.APIGatewayProxyResponse{
		Headers:    headers,
		Body:       string(body),
		StatusCode: http.StatusOK,
	}, nil
}

Our handler is designed to take in an incoming request that’s been proxied through our API Gateway. This means the incoming payload will be of type events.APIGatewayProxyRequest. In kind, our response must be of type events.APIGatewayProxyResponse so that API Gateway can complete the client’s request.

Inside of the function itself, we’re doing the work of parsing the Slack command to determine what the user’s intent is before formulating a response that we return as a JSON payload. When the user requests help or sends an unknown or bad command, this function will immediately return a payload designed to give the user some usage hints. This is done synchronously. You can see this in help_command.go where in the execute method with the following signature:

func (c *helpCommand) execute(ctx context.Context) (*SlackResponse, error) {
	...
}

Let's look at the integration with SQS which is triggered when the user issues an actual shoutout. Look at shoutout_command.go and its execute method:

func (c *shoutoutCommand) execute(ctx context.Context) (*SlackResponse, error) {
	bs, err := json.Marshal(c.shoutout)
	if err != nil {
		return nil, fmt.Errorf("failed to marshal shoutout: %s", err)
	}

	in := sqs.SendMessageInput{
		MessageBody: aws.String(string(bs)),
		QueueUrl:    aws.String(os.Getenv("QUEUE_URL")),
	}

	var res *SlackResponse

	if _, err := c.sqs.SendMessageWithContext(ctx, &in); err != nil {
		res = &SlackResponse{
			ResponseType: SlackResponseTypeEphemeral,
			Text:         "Uh oh. There was a problem sending your shoutout. Try again later.",
		}
		return res, fmt.Errorf("failed to publish shoutout: %s", err)
	}

	res = &SlackResponse{
		ResponseType: SlackResponseTypeEphemeral,
		Text:         "You sent a shoutout!",
	}

	return res, nil
}

We’re making use of the sqs package in the AWS Go SDK to put a message into a queue for another process to handle. I’ll leave you to explore the rest of the code in this Lambda function on your own.

The next function we’ll look at is our processor/main.go in conjuntion with processor.go which handle picking up messages from a queue for processing. We start out in the same way with a main() that looks identical to the previous function we looked at. This time, the handler signature looks a little different:

func handler(ctx context.Context, sqsEvent *events.SQSEvent) error {
	return shoutouts.NewSQSProcessor(sf).Process(ctx, sqsEvent)
}

The SQSProcessor type takes in an AWS Step Functions dependency which it uses internally to trigger a state machine that executes the multiple steps involved in capturing a shoutout.

func (p *SQSProcessor) Process(ctx context.Context, event *events.SQSEvent) error {
	for _, message := range event.Records {
		log.Printf("Processing message %s | %s", message.MessageId, message.Body)

		in := sfn.StartExecutionInput{
			Input:           aws.String(message.Body),
			StateMachineArn: aws.String(os.Getenv("STATE_MACHINE_ARN")),
		}

		out, err := p.sfn.StartExecutionWithContext(ctx, &in)
		if err != nil {
			return fmt.Errorf("Failed to start state machine execution: %s", err)
		}

		log.Printf("Started State Machine execution | ARN: %s", *out.ExecutionArn)
	}

	return nil
}

Here in the Process method for SQSProcessor we’re expecting an *events.SQSEvent as our second parameter because the processor is invoked by Lambda when there is one or more messages in pre-configured queue. How does Lambda know this? Well, recall back in our SAM template we had a function specification like this:

  ShoutoutProcessorFunction:
    Type: AWS::Serverless::Function
    DependsOn: ShoutoutsRole
    Properties:
      Role: !GetAtt ShoutoutsRole.Arn
      CodeUri: ../build/processor.zip
      Handler: processor
      Runtime: go1.x
      Tracing: Active
      MemorySize: 128
      Events:
        SQSEvent:
          Type: SQS
          Properties:
            Queue: !GetAtt ShoutoutsQueue.Arn
            BatchSize: 10
      Environment:
        Variables:
          STATE_MACHINE_ARN: !Ref ShoutoutsStateMachine

This resource defined our processor function with one event source, the SQS queue we also defined in the template. That is the same queue we feed shoutout requests into from the code we looked at in shoutout_command.go.

The processor’s one job is to kick off a multi-step process that includes capturing the shoutout data, collecting some metrics, and finally executing a callback to the remote Slack service, relaying a response to the user that originated the request. We’ll dive deeper into this orchestration a bit later. Right now though, it’s time for you to write a Lambda function of your own.

Clone this wiki locally