How I build Go lambdas

Some notes on how I build go lambda functions.

I have been writing go lambdas for about six months and I have learnt a thing or two during that time. Below is an example of a lambda that sends an email, I will attempt to explain some of my design decisions.

The architecture of this repo looks like;

handler/
	handler.go
	handler_test.go
mail/
	mail.go
	mail_test.go
vendor/
	// dependencies
glide.lock
glide.yaml
main.go

Glide for Dependency Management

One of the first things I do when beginning work on any kind of go application is to set up glide. Glide is a dependency manager for go, think composer for PHP. I prefer installing my dependencies on a per project basis to save my gopath bin and src directories from getting cluttered with every dependency for every project I have ever worked on.

After installing glide it is easy to get started (assuming that there are no go errors in your source code);

glide init
glide install

Something to consider when writing your main function

Something important to remember is that the main function is only ran ONCE per container, and that is when the container first starts up. So unless you are running the container infrequently I would strongly suggest that you should be very careful in what you initialise within the main function. I have had problems with sql timeouts and sftp connections because I had initialised them in the main function and left to timeout and create all sorts of havoc.

In the meantime though I always set my logger up in the main, because it does not timeout and it saves me from initialising it every time a request comes in. The logger is then injected into the handler and all output in channelled through that.

package main

import (
	"github.com/aws/aws-lambda-go/lambda"
	"github.com/sirupsen/logrus"
	"os"
	"sending-mail/handler"
	"sending-mail/mail"
)

func main () {
	h := handler.Handler{
		Logger: setUpLogger(),
		Mailer: setUpMailer(),
	}
	lambda.Start(h.Handle)
}

func setUpLogger() *logrus.Logger {
	logger := logrus.New()
	logger.SetFormatter(&logrus.JSONFormatter{})
	return logger
}

func setUpMailer() mail.EmailSender {
	conf := mail.EmailConfig{
		Username: os.Getenv("SMTP_USERNAME"),
		Password: os.Getenv("SMTP_PASSWORD"),
		ServerHost: os.Getenv("SMTP_HOST"),
		ServerPort: os.Getenv("SMTP_PORT"),
		SenderAddr: "",
	}
	 return mail.EmailSender{
		Conf: conf,
	}
}

Wrap your execution in a handler struct

Every Go lambda I write has a Handler struct. This makes testing my solution much easier as all my handler structs fields are interfaces meaning that I can insert stubs for the functionality i require and test the handler logic. It also means in this example that I will not be firing off real emails every time I run my unit tests.

package handler

import (
	"github.com/aws/aws-lambda-go/events"
	"github.com/sirupsen/logrus"
	"net/http"
	"sending-mail/mail"
)

type Handler struct {
	Logger *logrus.Logger
	Mailer mail.EmailSender
}

func (h Handler) Handle() (events.APIGatewayProxyResponse, error) {
	// Connect to the remote SMTP server.
	err := h.Mailer.Send("to.some-guy@test.com", []byte("body goes here."))

	if err != nil {
		h.Logger.Error(err)
	}

	return events.APIGatewayProxyResponse{
		StatusCode: http.StatusCreated,
	}, err
}

Only unit test what needs testing

No one loves test driven development more than me however…. Only test what needs to be tested for example I will not be writing any unit tests for the mail package. Since this function only uses the built-in go lib, I will essentially be testing to internals.

A hard and fast rule to abide by is that if a function does not have any flow, for example if, else, while, and for statements.

package mail

import "net/smtp"

type EmailConfig struct {
	Username   string
	Password   string
	ServerHost string
	ServerPort string
	SenderAddr string
}

type Sender interface {
	Send(to []string, body []byte) error
}

type EmailSender struct {
	Conf EmailConfig
	send func(string, smtp.Auth, string, []string, []byte) error
}

func (e *EmailSender) Send(to string, body []byte) error {
	addr := e.Conf.ServerHost + ":" + e.Conf.ServerPort
	auth := smtp.PlainAuth("", e.Conf.Username, e.Conf.Password, e.Conf.ServerHost)
	return e.send(addr, auth, e.Conf.SenderAddr, []string{to}, body)
}