DynamoDB UpdateItem en Go (AWS SDK v2)

UpdateItem est simple en Go ; le point délicat, c'est que map[string]types.AttributeValue est une map d'interfaces, donc les valeurs que tu envoies comme celles que tu récupères sont des pointeurs vers l'une des neuf structs membres. Ce seul choix de conception explique l'essentiel des frictions ci-dessous, à commencer par le fait que le fmt.Println de cet extrait n'imprime pas ton élément.

Code

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
}

Explication

  • AttributeValueMemberN.Value est une string, pas un type numérique, et attributevalue.Marshal la garde ainsi : int64(9007199254740993) se marshalle exactement en "9007199254740993". Les nombres DynamoDB portent 38 chiffres de précision, plus qu'aucun flottant Go, donc le SDK ne convertit jamais. C'est toi qui fais l'analyse, à la frontière, délibérément.

  • ReturnValues: types.ReturnValueAllNew — la valeur de la constante est le littéral "ALL_NEW". types.ReturnValueAllNew.Values() liste les cinq (NONE, ALL_OLD, UPDATED_OLD, ALL_NEW, UPDATED_NEW) si tu préfères les lire sur le type plutôt que dans la documentation.

  • UpdateExpression est un *string que tu construis toi-même, ou que tu confies au paquet expression ci-dessous. Dans tous les cas, DynamoDB est le seul analyseur : ADD #upd2 :updValue2 donne l'incrément atomique, attribute_exists(Artist) dans une ConditionExpression rend l'appel purement modificateur au lieu d'un upsert, et la grammaire des clauses est dans les expressions de mise à jour.

  • ValidationException n'a pas de type Go. types/errors.go définit 35 structs d'erreur, dont ConditionalCheckFailedException, TransactionCanceledException, ProvisionedThroughputExceededException et TransactionConflictException. Les échecs de validation n'en font pas partie, donc une mauvaise expression apparaît comme un smithy.APIError générique que tu ne peux reconnaître qu'à la chaîne :

    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

    Note le préfixe api error, que la ConditionalCheckFailedException modélisée ci-dessous n'obtient pas. Sa présence est un signal correct que errors.As contre un type concret ne va pas t'aider ici.

  • Une condition échouée, en revanche, est un vrai type. var ccf *types.ConditionalCheckFailedException; errors.As(err, &ccf) correspond, et avec ReturnValuesOnConditionCheckFailure: types.ReturnValuesOnConditionCheckFailureAllOld sur l'entrée, ccf.Item arrive rempli avec l'élément tel qu'il était réellement. ccf.ErrorMessage() donne le simple The conditional request failed, sans le préambule de transport qu'err.Error() ajoute devant.

Relire le résultat

fmt.Println(out.Attributes) sur une map d'interfaces imprime des adresses :

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

Deux sorties possibles. Démarshaller dans une struct, ce que la plupart du code devrait faire :

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 faire une assertion sur le seul membre qui t'intéresse, en te souvenant que .Value est une chaîne :

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

Laisse le paquet expression l'écrire

Go est le seul SDK de ce site qui générera l'UpdateExpression pour toi. feature/dynamodb/expression compose les clauses et les deux maps :

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()

Ce qui en sort n'est pas ce que tu as écrit :

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

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

Le builder a réordonné les clauses, les a séparées par un saut de ligne, et a numéroté lui-même les placeholders, si bien que #0 est Awards plutôt que le premier nom que tu as mentionné. Rien en aval ne s'en soucie, mais ces chaînes ne sont pas stables d'une modification à l'autre, ce qui en fait de mauvaises cibles d'assertion dans les tests. Passe expr.Update(), expr.Names() et expr.Values() directement dans UpdateItemInput et ne les regarde jamais.

L'avantage, c'est qu'il aliase chaque nom : les mots réservés cessent d'être une classe de bug que tu peux expédier. Si tu écris l'expression à la main, passe d'abord les noms d'attribut par le vérificateur de mots réservés DynamoDB — la liste AWS compte 573 entrées et Year, Name et Status y sont tous. Et si tu préfères regarder un élément comme de la donnée plutôt que comme une map de pointeurs d'interface, télécharge DynoTable.

Exemples liés

Références

Dernière vérification le 2026-07-28 par rapport à la documentation officielle AWS liée ci-dessus.

Travaille avec DynamoDB sans la Console

Un client de bureau rapide pour DynamoDB qui exécute le vrai SQL que DynamoDB ne peut pas — JOINs, GROUP BY, agrégations — avec édition visuelle et un agent IA sur tes propres clés Bedrock.

Essai gratuit de 30 jours, sans carte bancaire — ensuite la formule Gratuit, sans limite de durée.