PutItem do DynamoDB em Go (AWS SDK v2)

O PutItem grava um item inteiro e substitui qualquer item existente com a mesma chave primária (ações baseadas em item explica como isso difere do UpdateItem). No AWS SDK for Go v2 a parte interessante não é a chamada, e sim no que os seus valores Go se transformam no caminho de saída.

Código

package main

import (
	"context"
	"errors"
	"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)

	_, err = client.PutItem(ctx, &dynamodb.PutItemInput{
		TableName: aws.String("Music"),
		Item: map[string]types.AttributeValue{
			"Artist":     &types.AttributeValueMemberS{Value: "Arturo Sandoval"},
			"SongTitle":  &types.AttributeValueMemberS{Value: "Cubano Chant"},
			"AlbumTitle": &types.AttributeValueMemberS{Value: "Danzon"},
			"Year":       &types.AttributeValueMemberN{Value: "1994"},
			"Awards":     &types.AttributeValueMemberN{Value: "0"},
		},
		ConditionExpression: aws.String("attribute_not_exists(#cond0) AND attribute_not_exists(#cond1)"),
		ExpressionAttributeNames: map[string]string{
			"#cond0": "Artist",
			"#cond1": "SongTitle",
		},
	})
	if err != nil {
		var ccf *types.ConditionalCheckFailedException
		if errors.As(err, &ccf) {
			fmt.Println("A song with that key already exists — not overwritten")
			return
		}
		log.Fatalf("put item: %v", err)
	}
	fmt.Println("Song written")
}

Explicação

O attributevalue.MarshalMap é o atalho, e ele tem opiniões. Entregar uma struct ao github.com/aws/aws-sdk-go-v2/feature/dynamodb/attributevalue em vez de montar à mão o map[string]types.AttributeValue acima é o movimento normal. Isto é o que ele de fato produziu para uma struct com um time.Time, um campo string intocado e um *int nil:

Artist     => &types.AttributeValueMemberS{Value:"Arturo Sandoval"}
SongTitle  => &types.AttributeValueMemberS{Value:"Cubano Chant"}
Released   => &types.AttributeValueMemberS{Value:"1994-01-01T00:00:00Z"}
Notes      => &types.AttributeValueMemberS{Value:""}
Rating     => &types.AttributeValueMemberNULL{Value:true}

Três coisas para tirar daí. time.Time vira uma string RFC 3339, não um número Unix, então uma chave de classificação de timestamp ordena lexicograficamente e só vai se comportar se todo valor estiver preenchido com zeros à esquerda e no mesmo fuso. Uma string intocada vira um atributo de string vazia de verdade, em vez de ficar de fora. E um ponteiro nil vira NULL, que é um atributo que existe.

Um atributo NULL derrota o attribute_not_exists. Essa é a que custa uma tarde. Grave um item cujo Rating veio de um *int nil, depois proteja a próxima escrita com attribute_not_exists(Rating) e ela falha:

ConditionalCheckFailedException: The conditional request failed

O DynamoDB está certo: o atributo está lá, contendo NULL. A correção é a struct tag, dynamodbav:"Rating,omitempty", que descarta o campo em vez de anulá-lo. A mesma tag impede o atributo de string vazia, o que importa porque essas quebram índices esparsos e, em um atributo de chave, são rejeitadas de imediato:

ValidationException: One or more parameter values are not valid. The AttributeValue for a key attribute cannot contain an empty string value. Key: Artist

errors.As, e nunca comparação de strings. O SDK de Go envolve todo erro de serviço no erro de operação do smithy, então a string que você recebe de err.Error() não é a mensagem que o DynamoDB enviou:

operation error DynamoDB: PutItem, https response error StatusCode: 400, RequestID: 6886fce0-b246-4762-8a8b-0c6dab813040, ConditionalCheckFailedException: The conditional request failed

O errors.As(err, &ccf) desembrulha isso e retornou true acima; o ccf.ErrorMessage() então devolve o The conditional request failed cru. Uma checagem com strings.Contains na string externa funciona hoje e quebra no dia em que o SDK mudar o formato do seu wrapper.

O erro tipado pode carregar o item que te bloqueou. Defina ReturnValuesOnConditionCheckFailure: types.ReturnValuesOnConditionCheckFailureAllOld na entrada e o ccf.Item volta preenchido: cinco atributos na execução acima, incluindo Year como &types.AttributeValueMemberN{Value:"1994"}. Isso poupa a ida e volta de leitura-após-falha que a maioria dos laços de retry faz na mão. O ReturnValues: types.ReturnValueAllOld é o equivalente no caminho de sucesso (ReturnValues).

Números são strings, e isso não é uma esquisitice do Go. AttributeValueMemberN{Value: "1994"} parece errado para quem vem de uma linguagem tipada, mas o tipo N do DynamoDB é um decimal transportado como texto justamente para que nada precise passar por um float64 na ida e na volta. strconv.FormatInt e strconv.FormatFloat fazem a conversão; o attributevalue faz isso por você.

O que você marshalla é o que você paga. Um put de um item de ~15 KB reportou "CapacityUnits": 15 com o ReturnConsumedCapacity. As escritas arredondam para cima a cada 1 KB, então um campo blob acidental, ou um MarshalMap que emitiu atributos que você queria descartar, aparece direto na conta.

Faça isso visualmente

Como o MarshalMap decide o que de fato entra no item, vale saber quanto esse item pesa. A calculadora de tamanho de item do DynamoDB gratuita recebe o JSON marshalado e devolve o tamanho em bytes e as unidades de escrita para as quais ele arredonda.

Para gravar e editar itens contra as suas próprias tabelas — um formulário por atributo, seletores de tipo e o resultado copiado de volta como Go — baixe o DynoTable.

Exemplos relacionados

Referências

Reproduzido em 2026-07-28 no go1.26.5 com aws-sdk-go-v2/service/dynamodb v1.62.1 e feature/dynamodb/attributevalue v1.20.55, contra o DynamoDB Local (amazon/dynamodb-local) na porta 9000. Os valores marshalados, as strings de erro e a leitura de capacidade são saída capturada, copiada literalmente.

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.