UpdateItem do DynamoDB em Go (AWS SDK v2)

O UpdateItem é direto em Go; a parte incômoda é que map[string]types.AttributeValue é um mapa de interfaces, então tanto os valores que você envia quanto os que recebe de volta são ponteiros para uma de nove structs membro. Essa única decisão de design explica a maior parte do atrito abaixo, a começar pelo fato de que o fmt.Println deste trecho não imprime o seu item.

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)

	out, err := client.UpdateItem(ctx, &dynamodb.UpdateItemInput{
		TableName: aws.String("Music"),
		Key: map[string]types.AttributeValue{
			"Artist":    &types.AttributeValueMemberS{Value: "Arturo Sandoval"},
			"SongTitle": &types.AttributeValueMemberS{Value: "Cubano Chant"},
		},
		UpdateExpression: aws.String("SET #upd0 = :updValue0, #upd1 = :updValue1 ADD #upd2 :updValue2"),
		ExpressionAttributeNames: map[string]string{
			"#upd0": "Genre",
			"#upd1": "Year",
			"#upd2": "Awards",
		},
		ExpressionAttributeValues: map[string]types.AttributeValue{
			":updValue0": &types.AttributeValueMemberS{Value: "Latin Jazz"},
			":updValue1": &types.AttributeValueMemberN{Value: "1994"},
			":updValue2": &types.AttributeValueMemberN{Value: "1"},
		},
		ReturnValues: types.ReturnValueAllNew,
	})
	if err != nil {
		log.Fatalf("update item: %v", err)
	}
	fmt.Println(out.Attributes) // the item after the update
}

Explicação

  • AttributeValueMemberN.Value é uma string, não um tipo numérico, e o attributevalue.Marshal a mantém assim: int64(9007199254740993) é marshalado para "9007199254740993" exatamente. Os números do DynamoDB carregam 38 dígitos de precisão, mais do que qualquer float do Go, então o SDK nunca converte. Você faz o parsing, na borda, deliberadamente.

  • ReturnValues: types.ReturnValueAllNew — o valor da constante é o literal "ALL_NEW". types.ReturnValueAllNew.Values() lista as cinco (NONE, ALL_OLD, UPDATED_OLD, ALL_NEW, UPDATED_NEW) se você preferir lê-las do tipo em vez da documentação.

  • UpdateExpression é um *string que você mesmo monta, ou entrega ao pacote expression abaixo. De qualquer forma, o DynamoDB é o único parser: ADD #upd2 :updValue2 dá o incremento atômico, attribute_exists(Artist) em uma ConditionExpression faz a chamada ser só-atualização em vez de upsert, e a gramática das cláusulas está em expressões de atualização.

  • ValidationException não tem tipo em Go. O types/errors.go define 35 structs de erro, incluindo ConditionalCheckFailedException, TransactionCanceledException, ProvisionedThroughputExceededException e TransactionConflictException. Falhas de validação não estão entre elas, então uma expressão ruim aparece como um smithy.APIError genérico que você só consegue reconhecer por string:

    operation error DynamoDB: UpdateItem, https response error StatusCode: 400, RequestID: 702d67f1-4f15-43dc-b3e9-cea691878801, api error ValidationException: Invalid UpdateExpression: Attribute name is a reserved keyword; reserved keyword: Year

    Repare no prefixo api error, que o ConditionalCheckFailedException modelado abaixo não recebe. A presença dele é um bom sinal de que errors.As contra um tipo concreto não vai te ajudar aqui.

  • Uma condição que falha, em contraste, é um tipo de verdade. var ccf *types.ConditionalCheckFailedException; errors.As(err, &ccf) casa, e com ReturnValuesOnConditionCheckFailure: types.ReturnValuesOnConditionCheckFailureAllOld na entrada, o ccf.Item chega preenchido com o item como ele de fato estava. O ccf.ErrorMessage() é o The conditional request failed cru, sem o preâmbulo de transporte que o err.Error() acrescenta.

Lendo o resultado de volta

fmt.Println(out.Attributes) sobre um mapa de interfaces imprime endereços:

map[Artist:0x1c8cf589e060 Awards:0x1c8cf589e078 Genre:0x1c8cf589e090 SongTitle:0x1c8cf589e0c0 Year:0x1c8cf589e0a8]

Duas saídas. Faça unmarshal para uma struct, que é o que a maioria dos códigos deveria fazer:

var song struct {
	Artist string
	Genre  string
	Year   int
	Awards int
}
err = attributevalue.UnmarshalMap(out.Attributes, &song)
// {Artist:Arturo Sandoval Genre:Latin Jazz Year:1994 Awards:1}

Ou faça a asserção do único membro que te interessa, lembrando que .Value é uma string:

n, ok := out.Attributes["Awards"].(*types.AttributeValueMemberN)
if ok {
	awards, _ := strconv.Atoi(n.Value) // "1" -> 1
	fmt.Println(awards)
}

Deixe o pacote expression escrever isso

O Go é o único SDK deste site que gera a UpdateExpression para você. O feature/dynamodb/expression compõe as cláusulas e os dois mapas:

upd := expression.Set(expression.Name("Genre"), expression.Value("Latin Jazz")).
	Set(expression.Name("Year"), expression.Value(1994)).
	Add(expression.Name("Awards"), expression.Value(1))
expr, _ := expression.NewBuilder().WithUpdate(upd).Build()

O que sai não é o que você escreveu:

UpdateExpression: ADD #0 :0
SET #1 = :1, #2 = :2

Names: map[#0:Awards #1:Genre #2:Year]

O builder reordenou as cláusulas, separou-as com uma quebra de linha e numerou os placeholders por conta própria, então #0 é Awards e não o primeiro nome que você mencionou. Nada mais adiante se importa, mas as strings não são estáveis entre edições, o que as torna algo ruim para asserção em testes. Passe expr.Update(), expr.Names() e expr.Values() direto para o UpdateItemInput e nunca olhe para elas.

A vantagem é que ele coloca alias em todo nome, então palavras reservadas deixam de ser uma classe de bug que você pode publicar. Se em vez disso você escrever a expressão à mão, passe os nomes de atributo pelo verificador de palavras reservadas do DynamoDB primeiro — a lista da AWS tem 573 entradas e Year, Name e Status estão todas nela. E se você preferir olhar um item como dados em vez de um mapa de ponteiros de interface, baixe o DynoTable.

Exemplos relacionados

Referências

Verificado pela última vez em 2026-07-28 contra a documentação oficial da AWS vinculada acima.

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.