In this example we are going to create an AWS Lambda function and invoke it using a Golang application. The example depends on a Localstack environment.


Lambda


// main.go

package main

import (
"context"
"fmt"
"log"
"time"

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

type Request struct {
Username string `json:"username"`
Password string `json:"password"`
}

type Response struct {
OK bool `json:"ok"`
Time time.Time `json:"time"`
}

func Login(ctx context.Context, req Request) (Response, error) {
select {
case <-ctx.Done():
return Response{OK: false, Time: time.Now()}, ctx.Err()
default:
}

log.Printf("%+v\n", req)

if req.Username != "hello" || req.Password != "world" {
return Response{OK: false, Time: time.Now()}, fmt.Errorf("invalid attempt")
}

return Response{OK: true, Time: time.Now()}, nil
}

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

Create and test function


$ GOOS=linux CGO_ENABLED=0 go build -ldflags "-s -w" -o main main.go

$ zip main.zip main

$ aws --profile localstack --endpoint-url http://localhost:4566 lambda create-function --function-name login --handler main --runtime go1.x --role create-role --zip-file fileb://main.zip

$ aws --profile localstack --endpoint-url http://localhost:4566 lambda invoke --function-name login --cli-binary-format raw-in-base64-out --payload '{"username":"hello","password":"world"}' response.json

Golang application


cloud/session.go


package cloud

import (
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/session"
)

func NewAWSSession() (*session.Session, error) {
return session.NewSessionWithOptions(session.Options{
Profile: "localstack",
Config: aws.Config{
Region: aws.String("eu-west-1"),
Endpoint: aws.String("http://localhost:4566"),
},
})
}

cloud/lambda.go


package cloud

import (
"fmt"
"regexp"
"strings"

"github.com/aws/aws-sdk-go/aws/session"
"github.com/aws/aws-sdk-go/service/lambda"
)

// errMsg is used to extract key-value pair for the JSON key `errorMessage` in
// the `invoke` result string.
var errMsg = regexp.MustCompile(`"errorMessage":[^,}]*`)

// NewLambdaClient returns a new Lambda client.
func NewLambdaClient(ses *session.Session) *lambda.Lambda {
return lambda.New(ses)
}

// CheckLambdaError accepts a result from a Lambda function call in order to work
// out if there was an error or not.
func CheckLambdaError(result map[string]interface{}) (string, error) {
msg, ok := result["errorMessage"]
if !ok {
return "", nil
}

match := errMsg.FindStringSubmatch(msg.(string))
if len(match) == 0 {
return "", fmt.Errorf("error result does not contain a json data")
}

parts := strings.Split(match[0], ":")
if msg := parts[1]; msg == "" || strings.TrimSpace(msg) == "" {
return "", fmt.Errorf("error message is missing in result")
}

return parts[1][1 : len(parts[1])-1], nil
}

Lambda errors look like below, so that's why we are doing a bit of data extraction above in order to get the actual error message.


Lambda process returned with error. Result: {"errorType":"errorString","errorMessage":"invalid attempt"}. Output:
START RequestId: 68d51f06-e6d6-141f-e660-60274d2ca777 Version: $LATEST
END RequestId: 68d51f06-e6d6-141f-e660-60274d2ca777
REPORT RequestId: 68d51f06-e6d6-141f-e660-60274d2ca777 Init Duration: 125.07 ms Duration: 9.88 ms Billed Duration: 10 ms Memory Size: 1536 MB Max Memory Used: 19 MB

user/login.go


package user

import (
"context"
"encoding/json"
"fmt"

"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/service/lambda"
"github.com/you/client/cloud"
)

type LoginRequest struct {
Username string `json:"username"`
Password string `json:"password"`
}

type LoginResponse struct {
OK bool `json:"ok,omitempty"`
Time string `json:"time,omitempty"`
Error string `json:"error,omitempty"`
}

func Login(ctx context.Context, client *lambda.Lambda, request LoginRequest) (LoginResponse, error) {
payload, err := json.Marshal(request)
if err != nil {
return LoginResponse{}, fmt.Errorf("marshal request: %w", err)
}

input := &lambda.InvokeInput{
FunctionName: aws.String("login"),
Payload: payload,
}

output, err := client.InvokeWithContext(ctx, input)
if err != nil {
return LoginResponse{}, fmt.Errorf("invoke: %w", err)
}

// fmt.Printf("OUTPUT: %+v\n", output)

var result map[string]interface{}
if err := json.Unmarshal(output.Payload, &result); err != nil {
return LoginResponse{}, fmt.Errorf("unmarshal result: %w", err)
}

// fmt.Printf("RESULT: %+v\n", result)

msg, err := cloud.CheckLambdaError(result)
if err != nil {
return LoginResponse{}, fmt.Errorf("check lambda error: %w", err)
}
if msg != "" {
return LoginResponse{Error: msg}, nil
}

var response LoginResponse

if ok := result["ok"]; ok != nil {
response.OK = ok.(bool)
}
if tm := result["time"]; tm != nil {
response.Time = tm.(string)
}

return response, nil
}

main.go


package main

import (
"context"
"encoding/json"
"fmt"
"log"

"github.com/you/client/cloud"
"github.com/you/client/user"
)

func main() {
ses, err := cloud.NewAWSSession()
if err != nil {
log.Fatalln(err)
}

res, err := user.Login(context.Background(), cloud.NewLambdaClient(ses), user.LoginRequest{
Username: "hello",
Password: "world",
})
if err != nil {
log.Fatalln(err)
}

dat, _ := json.MarshalIndent(res, "", " ")
fmt.Println(string(dat))
}

Test


// Send some invalid username or password other than hello:world

$ go run main.go
{
"error": "invalid attempt"
}

// Send hello:world

$ go run main.go
{
"ok": true,
"time": "2021-12-30T21:42:28.4341183Z"
}