Scan do DynamoDB em Go (AWS SDK v2)

O NewScanPaginator é um cursor, não uma coleção: ele começa otimista, termina esgotado e tira o tamanho de página de um lugar que o ScanInput à sua frente não mostra. Para saber quando evitar o Scan por completo, veja Query vs. Scan.

Código

package main

import (
	"context"
	"fmt"
	"log"

	"github.com/aws/aws-sdk-go-v2/aws"
	"github.com/aws/aws-sdk-go-v2/config"
	"github.com/aws/aws-sdk-go-v2/service/dynamodb"
	"github.com/aws/aws-sdk-go-v2/service/dynamodb/types"
)

func main() {
	ctx := context.TODO()
	cfg, err := config.LoadDefaultConfig(ctx, config.WithRegion("us-east-1"))
	if err != nil {
		log.Fatalf("load config: %v", err)
	}
	client := dynamodb.NewFromConfig(cfg)

	paginator := dynamodb.NewScanPaginator(client, &dynamodb.ScanInput{
		TableName:        aws.String("Music"),
		FilterExpression: aws.String("#filter0 >= :filterValue0"),
		ExpressionAttributeNames: map[string]string{
			"#filter0": "Year",
		},
		ExpressionAttributeValues: map[string]types.AttributeValue{
			":filterValue0": &types.AttributeValueMemberN{Value: "2010"},
		},
	})

	var items []map[string]types.AttributeValue
	for paginator.HasMorePages() {
		page, err := paginator.NextPage(ctx)
		if err != nil {
			log.Fatalf("scan: %v", err)
		}
		items = append(items, page.Items...)
	}
	fmt.Printf("Matched %d items\n", len(items))
}

O que o HasMorePages() de fato retorna

Contra um fixture de 600 músicas de aproximadamente 3,9 KB cada, das quais 8 casam com Year >= 2010 e ficam por último na ordenação:

fresh HasMorePages():                    true    (before any request)
page 1: len(page.Items)=0  ScannedCount=271  CU=128.5  LastEvaluatedKey set
page 2: len(page.Items)=0  ScannedCount=271  CU=128.5  LastEvaluatedKey set
page 3: len(page.Items)=8  ScannedCount= 58  CU= 27.5  LastEvaluatedKey nil
exhausted HasMorePages():                false
NextPage() after exhaustion:             err = "no more pages available"

O HasMorePages() é true antes da primeira chamada, então o laço sempre dispara ao menos uma vez. Você não pode usá-lo para testar se uma tabela está vazia. Duas páginas então retornaram zero itens enquanto o scan ainda estava rodando, o que significa que if len(page.Items) == 0 { break } reporta resultado vazio em uma tabela que tem 8 correspondências. E o paginador é de uso único: depois do laço o valor está gasto, e o NextPage retorna um erro em vez de recomeçar. Escanear de novo significa chamar NewScanPaginator de novo.

O Limit mora em dois lugares, e a opção ganha

O Go divide o tamanho de página entre a struct de entrada e as opções do próprio paginador. Defina os dois e a opção tem precedência:

ScanInput.Limit = 500, ScanPaginatorOptions.Limit = 25  ->  ScannedCount 25

Atribuir in.Limit = aws.Int32(500) e esperar páginas de 500 itens é um erro silencioso quando uma opção funcional em outro lugar já definiu 25. Se você mexe só no ScanInput, o paginador o respeita.

Medido em 2026-07-28 contra o DynamoDB Local (amazon/dynamodb-local) com aws-sdk-go-v2/service/dynamodb v1.62.1 no go1.26.5.

Explicação

  • page.Items é []map[string]types.AttributeValue, não a sua struct. O attributevalue.UnmarshalListOfMaps(items, &songs) converte o lote em uma chamada e transforma Year em um int do Go; as struct tags dynamodbav controlam o mapeamento.
  • Números são strings na rede. O valor cru é &types.AttributeValueMemberN{Value: "2010"} — o membro N guarda uma string do Go, então o strconv fica dos dois lados de qualquer aritmética, a menos que você faça unmarshal para uma struct tipada.
  • A FilterExpression roda depois da leitura, e é por isso que as duas páginas vazias ainda custaram 128,5 unidades cada. A referência da API é explícita ao dizer que filtrar "does not consume any additional read capacity units", e o corolário é que também não economiza nenhuma.
  • #filter0 é obrigatório, não estilístico. Year é uma palavra reservada do DynamoDB; sem alias ele retorna ValidationException: Invalid FilterExpression: Attribute name is a reserved keyword; reserved keyword: Year.
  • Segment / TotalSegments dão a cada goroutine a sua própria fatia da tabela, e cada uma precisa do seu próprio paginador. O paralelismo corta o tempo de relógio e gasta a mesma capacidade.

Faça isso visualmente

O construtor de consultas do DynamoDB emite o formato do programa inteiro — filtro, mapas de nome e de valor e o laço do paginador — para que as partes que esta página mede sejam geradas em vez de lembradas.

Para ver quantos itens um filtro realmente toca antes de você entregar o scan a um serviço, baixe o DynoTable e rode-o contra a tabela em uma grade.

Exemplos relacionados

Referências

Monte esta solicitação visualmente

Componha esta operação no Construtor de Consultas do DynamoDB gratuito — key condition, filtro, índice, Limit, ordem de classificação e um laço de paginação — e copie de volta como um programa executável para SDK v3, CLI ou boto3.

Abrir o Construtor de Consultas do DynamoDB

Trabalhe com o DynamoDB sem o Console

Um cliente desktop rápido para DynamoDB que roda o SQL de verdade que o DynamoDB não consegue — JOINs, GROUP BY, agregações — com edição visual e um agente de IA com suas próprias chaves do Bedrock.

Teste grátis de 30 dias, sem cartão de crédito — depois o plano Grátis sem limite de tempo.