Go com AWS Lambda: API Serverless em Produção
Go combina muito bem com AWS Lambda. A linguagem gera binários pequenos, sobe rápido, consome pouca memória e não exige runtime pesado. Para times brasileiros que precisam entregar APIs, workers, automações cloud ou integrações assíncronas sem operar servidores o tempo todo, Lambda pode ser um caminho pragmático.
Este guia mostra como estruturar uma API serverless em Go, do primeiro handler ao deploy. A ideia não é vender serverless como solução mágica: você vai entender onde Lambda ajuda, onde atrapalha e quais cuidados deixam o projeto mais próximo de produção.
Se você ainda está montando a base, leia antes API REST com Go, Go com Docker e Go com PostgreSQL. Se seu foco é carreira, a página de vagas Go com AWS mostra por que Lambda, SQS, DynamoDB e observabilidade aparecem tanto em descrições de backend, plataforma e DevOps.
Quando usar Go com Lambda
Lambda é melhor quando o trabalho é curto, previsível e acionado por HTTP, fila, evento ou agendamento. Bons casos de uso incluem:
- APIs pequenas ou médias atrás do API Gateway
- Webhooks de pagamento, CRM, GitHub ou ferramentas internas
- Workers que consomem SQS, SNS, EventBridge ou Kinesis
- Rotinas agendadas com EventBridge Scheduler
- Processamento de arquivos enviados para S3
- Automações internas que rodam poucas vezes por dia
Go ajuda nesses cenários porque o pacote final normalmente é um binário único. Isso simplifica deploy, reduz dependências e melhora cold start em comparação com runtimes mais pesados. Também é uma linguagem ótima para lidar com concorrência controlada, timeouts, contexto e chamadas HTTP para serviços externos.
Mas Lambda não é ideal para tudo. Evite quando você precisa de processos longos, conexões persistentes como WebSocket tradicional, workloads com muita memória, tarefas que passam do limite de execução, controle avançado de rede ou servidores que precisam ficar quentes o tempo todo. Nesses casos, ECS, EKS, Fly.io, uma VM simples ou outro modelo de deploy pode fazer mais sentido.
Estrutura do projeto
Vamos criar uma API simples de tarefas usando Lambda, API Gateway e DynamoDB. A estrutura fica assim:
minha-api-lambda/
├── cmd/
│ └── api/
│ └── main.go
├── internal/
│ ├── handler/
│ │ └── tasks.go
│ └── store/
│ └── dynamodb.go
├── go.mod
├── template.yaml
└── Makefile
Inicialize o módulo:
mkdir minha-api-lambda
cd minha-api-lambda
go mod init exemplo.com/minha-api-lambda
go get github.com/aws/aws-lambda-go/events
go get github.com/aws/aws-lambda-go/lambda
go get github.com/aws/aws-sdk-go-v2/config
go get github.com/aws/aws-sdk-go-v2/service/dynamodb
O pacote aws-lambda-go fornece o adaptador do runtime Lambda. O SDK v2 da AWS será usado para acessar DynamoDB com contexto, timeouts e configuração padrão do ambiente.
Primeiro handler HTTP
Crie cmd/api/main.go:
package main
import (
"context"
"encoding/json"
"net/http"
"github.com/aws/aws-lambda-go/events"
"github.com/aws/aws-lambda-go/lambda"
)
type resposta struct {
Mensagem string `json:"mensagem"`
}
func handler(ctx context.Context, req events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error) {
body, err := json.Marshal(resposta{Mensagem: "API Go rodando no AWS Lambda"})
if err != nil {
return events.APIGatewayProxyResponse{StatusCode: http.StatusInternalServerError}, err
}
return events.APIGatewayProxyResponse{
StatusCode: http.StatusOK,
Headers: map[string]string{
"Content-Type": "application/json; charset=utf-8",
},
Body: string(body),
}, nil
}
func main() {
lambda.Start(handler)
}
Para compilar para Lambda, gere um binário Linux:
GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build -o bootstrap ./cmd/api
zip function.zip bootstrap
Em runtimes modernos da AWS, você também pode usar provided.al2023 com binário chamado bootstrap. Isso deixa o deploy simples e evita dependência de um runtime Go gerenciado específico.
Rotas com API Gateway
O APIGatewayProxyRequest traz método, path, headers, query string e body. Um roteador pequeno já resolve muitos projetos:
func handler(ctx context.Context, req events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error) {
switch {
case req.HTTPMethod == http.MethodGet && req.Path == "/tarefas":
return listarTarefas(ctx)
case req.HTTPMethod == http.MethodPost && req.Path == "/tarefas":
return criarTarefa(ctx, req.Body)
default:
return jsonResponse(http.StatusNotFound, map[string]string{"erro": "rota nao encontrada"})
}
}
func jsonResponse(status int, payload any) (events.APIGatewayProxyResponse, error) {
body, err := json.Marshal(payload)
if err != nil {
return events.APIGatewayProxyResponse{StatusCode: http.StatusInternalServerError}, err
}
return events.APIGatewayProxyResponse{
StatusCode: status,
Headers: map[string]string{"Content-Type": "application/json; charset=utf-8"},
Body: string(body),
}, nil
}
Para APIs maiores, considere aws-lambda-go-api-proxy com chi, gorilla/mux ou outro roteador HTTP. Só evite começar com complexidade demais: muitas funções Lambda em produção são simples o bastante para um switch explícito por método e rota.
DynamoDB com contexto
DynamoDB aparece bastante em arquiteturas serverless porque escala sem servidor e integra bem com Lambda. Um repositório mínimo pode receber o cliente no boot da função:
package main
import (
"context"
"log"
"os"
"github.com/aws/aws-lambda-go/lambda"
"github.com/aws/aws-sdk-go-v2/config"
"github.com/aws/aws-sdk-go-v2/service/dynamodb"
)
var ddb *dynamodb.Client
var tabela string
func main() {
ctx := context.Background()
cfg, err := config.LoadDefaultConfig(ctx)
if err != nil {
log.Fatalf("carregando config AWS: %v", err)
}
ddb = dynamodb.NewFromConfig(cfg)
tabela = os.Getenv("TASKS_TABLE")
if tabela == "" {
log.Fatal("TASKS_TABLE nao configurada")
}
lambda.Start(handler)
}
Inicializar clientes fora do handler é importante. A AWS pode reutilizar o mesmo ambiente de execução em múltiplas invocações, então você economiza tempo e conexões quando não recria SDK, configurações e caches a cada request.
Ao chamar DynamoDB, sempre propague o context.Context recebido pelo handler. Assim timeouts, cancelamentos e deadlines do Lambda chegam até o SDK:
func listarTarefas(ctx context.Context) (events.APIGatewayProxyResponse, error) {
out, err := ddb.Scan(ctx, &dynamodb.ScanInput{
TableName: &tabela,
Limit: aws.Int32(25),
})
if err != nil {
return jsonResponse(http.StatusInternalServerError, map[string]string{"erro": "falha ao buscar tarefas"})
}
return jsonResponse(http.StatusOK, map[string]any{"items": out.Items})
}
Em produção, prefira Query com chave bem desenhada em vez de Scan amplo. Scan é aceitável para tutorial, painel interno pequeno ou tabela minúscula, mas pode ficar caro e lento conforme os dados crescem.
Deploy com AWS SAM
Uma forma direta de versionar infraestrutura é usar AWS SAM. Crie template.yaml:
AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: API Go serverless com Lambda e DynamoDB
Globals:
Function:
Runtime: provided.al2023
Architectures:
- x86_64
Timeout: 10
MemorySize: 128
Environment:
Variables:
TASKS_TABLE: !Ref TasksTable
Resources:
ApiFunction:
Type: AWS::Serverless::Function
Properties:
CodeUri: .
Handler: bootstrap
Policies:
- DynamoDBCrudPolicy:
TableName: !Ref TasksTable
Events:
ListTasks:
Type: Api
Properties:
Path: /tarefas
Method: GET
CreateTask:
Type: Api
Properties:
Path: /tarefas
Method: POST
TasksTable:
Type: AWS::DynamoDB::Table
Properties:
BillingMode: PAY_PER_REQUEST
AttributeDefinitions:
- AttributeName: pk
AttributeType: S
KeySchema:
- AttributeName: pk
KeyType: HASH
E um Makefile simples:
build:
GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build -o bootstrap ./cmd/api
deploy: build
sam deploy --guided
O primeiro sam deploy --guided pergunta stack name, região e permissões. Depois disso, o deploy pode rodar no CI com sam deploy --no-confirm-changeset --no-fail-on-empty-changeset.
Testes locais
Não deixe todo feedback depender da AWS. Extraia regras de negócio para funções comuns e teste sem Lambda:
func TestJsonResponse(t *testing.T) {
res, err := jsonResponse(http.StatusOK, map[string]string{"ok": "true"})
if err != nil {
t.Fatal(err)
}
if res.StatusCode != http.StatusOK {
t.Fatalf("status = %d", res.StatusCode)
}
if !strings.Contains(res.Body, "true") {
t.Fatalf("body inesperado: %s", res.Body)
}
}
Para simular o evento HTTP, chame o handler diretamente:
func TestHandlerNotFound(t *testing.T) {
req := events.APIGatewayProxyRequest{HTTPMethod: http.MethodGet, Path: "/nao-existe"}
res, err := handler(context.Background(), req)
if err != nil {
t.Fatal(err)
}
if res.StatusCode != http.StatusNotFound {
t.Fatalf("status = %d", res.StatusCode)
}
}
Para DynamoDB, use interfaces pequenas no seu pacote de store. Assim você testa handler com fake em memória e deixa testes de integração para uma camada separada, usando DynamoDB Local ou uma tabela temporária.
Observabilidade e segurança
Uma Lambda sem logs bons vira caixa-preta. No mínimo, registre método, rota, status, duração e erro. Use logs estruturados quando possível:
log.Printf("method=%s path=%s status=%d", req.HTTPMethod, req.Path, status)
Em produção, considere CloudWatch Logs Insights, métricas customizadas, alarmes de erro, DLQ para eventos assíncronos e tracing com AWS X-Ray ou OpenTelemetry. Também configure timeout realista: se sua chamada externa costuma levar 2 segundos, uma função com timeout de 30 segundos só demora mais para falhar e custa mais.
Na parte de segurança, use permissões IAM mínimas. Se a função só lê uma tabela, não dê dynamodb:*. Se só publica em uma fila, limite por ARN. Segredos devem vir de Secrets Manager, SSM Parameter Store ou variáveis criptografadas, nunca do código.
Também cuide de validação de entrada. API Gateway e Lambda não tornam payload confiável. Valide JSON, limite tamanho de body, trate Content-Type, normalize erros e não exponha mensagens internas para o usuário final.
Checklist de produção
Antes de publicar uma API Go em Lambda, confira:
- O binário é compilado com
GOOS=linuxeCGO_ENABLED=0 - Clientes AWS são inicializados fora do handler
context.Contexté propagado para chamadas externas- Logs têm rota, status, duração e erro
- Permissões IAM são específicas por recurso
- Timeouts e memória foram ajustados com base em teste real
- Erros 4xx e 5xx têm respostas JSON previsíveis
- Deploy é reproduzível por SAM, CDK, Terraform ou CI
- Há alarmes para erro, timeout e throttling
- Custos de API Gateway, Lambda, logs e DynamoDB foram estimados
Go com AWS Lambda é uma ótima opção quando você quer simplicidade operacional sem abrir mão de performance. Comece pequeno, meça cold starts e latência, mantenha a infraestrutura versionada e conecte o aprendizado com o mercado: muitas vagas Golang com AWS pedem exatamente essa combinação de backend, eventos, cloud e responsabilidade por produção.