4

Looking at the Using global state section in the official AWS Lambda function handler in Go doc https://docs.aws.amazon.com/lambda/latest/dg/golang-handler.html

suggests to initialise all global state in func init() i.e. Any package level vars which we want to share across multiple lambda invocations go here.
And my understanding is that this initialisation is done once per lambda container start (i.e cold start).

My question is, is it possible to do the same using func main() instead of func init().
Using func init() basically makes my handler function (func LambdaHandler) non unit-testable due to side-effects from func init() running.
Moving the func init() code to func main() seems to solve this easily.
Are there any side effects to using func main() vs func init()

Code Example

Using func init()

package main
 
import (
        "log"
        "github.com/aws/aws-lambda-go/lambda"
        "github.com/aws/aws-sdk-go/aws/session"
        "github.com/aws/aws-sdk-go/service/s3"
        "github.com/aws/aws-sdk-go/aws"
)
 
var invokeCount = 0
var myObjects []*s3.Object
func init() {
        svc := s3.New(session.New())
        input := &s3.ListObjectsV2Input{
                Bucket: aws.String("examplebucket"),
        }
        result, _ := svc.ListObjectsV2(input)
        myObjects = result.Contents
}
 
func LambdaHandler() (int, error) {
        invokeCount = invokeCount + 1
        log.Print(myObjects)
        return invokeCount, nil
}
 
func main() {
        lambda.Start(LambdaHandler)
}

vs

Using func main()

package main
 
import (
        "log"
        "github.com/aws/aws-lambda-go/lambda"
        "github.com/aws/aws-sdk-go/aws/session"
        "github.com/aws/aws-sdk-go/service/s3"
        "github.com/aws/aws-sdk-go/aws"
)
 
var invokeCount = 0
var myObjects []*s3.Object
 
func LambdaHandler() (int, error) {
        invokeCount = invokeCount + 1
        log.Print(myObjects)
        return invokeCount, nil
}
 
func main() {
        svc := s3.New(session.New())
        input := &s3.ListObjectsV2Input{
                Bucket: aws.String("examplebucket"),
        }
        result, _ := svc.ListObjectsV2(input)
        myObjects = result.Contents

        lambda.Start(LambdaHandler)
}
  • 1
    You can't unit test `main` either. Put any code you want to test in a function that you can call from a test and pass in the required dependencies for that code. – JimB Jun 03 '21 at 15:36
  • I do not want to test the code in `main` or `init`. I want to test the code in `LambdaHandler`. But this is not possible if there is a `func init()` as it causes side-effects when I try to test it – Sriraghavan Subramanian Jun 03 '21 at 15:36
  • You said "Using func init() basically makes my handler function non unit-testable"; `main` is no more testable than `init`. If you need runtime error handling, then put it in main. Wether you use `init` or `main` to setup some data structures is up to you, but I prefer to avoid `init` for `main` packages. – JimB Jun 03 '21 at 15:39
  • I have edited my question. Hope it is more readable now. I am looking to write tests for the code in `LambdaHandler` – Sriraghavan Subramanian Jun 03 '21 at 15:40
  • 1
    Relying on global variables is pretty much a guarantee to make whatever you write close to impossible to unit-test. Your logic should reside in a function/type/package that takes its dependencies as arguments, so you can pass in mocks and write unit tests. Your question is a classic X-Y problem: you're looking for a solution to X (how to test your code), but the problem is caused by Y (how you wrote/structured your code) – Elias Van Ootegem Jun 03 '21 at 15:40
  • Unfortunately, there does not seem to be any way other than using global states, in order to achieve shared memory across AWS Lambda invocations. And this is quite critical to the latency of the Lambda executing as creating a new s3 client or loading the data on each lambda execution is quite expensive (around 2~3 seconds) And seeing that the official AWS Lambda doc does seem to suggest using global states as the solution of choice for this, I am inclined to think this is the preferable way to do this. – Sriraghavan Subramanian Jun 03 '21 at 15:46

1 Answers1

10

I would propose the following (which we use successful in a lot of Go Lambdas).

main.go

[...]

func (h *handler) handleRequest(ctx context.Context) error {
    input := h.s3Client.ListObjectsV2Input{
        Bucket: aws.String("examplebucket"),
    }

    [...]
}

type handler struct {
    s3Client s3iface.S3API
}

// main is called only once, when the Lambda is initialised (started for the first time). Code in this function should
// primarily be used to create service clients, read environments variables, read configuration from disk etc.
func main() {
    h := handler{
        s3client: s3.New(session.New()),
    }

    lambda.Start(h.handleRequest)
}

main_test.go

type ListObjectsV2Mock struct {
    s3iface.S3API

    output *s3.ListObjectsV2Output
}

func TestHandleRequest(t *testing.T) {
    h := handler{
        s3Client: &ListObjectsV2Mock{
            output: &s3.ListObjectsV2Output{...},
        },
    }

    err := h.HandleRequest(context.TODO())
    if err != nil {
        t.Fatalf("unexpected error: %v", err)
    }
}

Obviously, a lot of code is missing (imports, error handling etc), but this is the gist of it.

Jens
  • 20,533
  • 11
  • 60
  • 86