DynamoDB UpdateItem in Go (AWS SDK v2)

UpdateItem ist in Go unkompliziert; das Unangenehme ist, dass map[string]types.AttributeValue eine Map von Interfaces ist — sowohl die Werte, die du schickst, als auch die, die du zurückbekommst, sind Pointer auf eines von neun Member-Structs. Diese eine Design-Entscheidung erklärt fast die gesamte Reibung weiter unten, angefangen damit, dass das fmt.Println in diesem Snippet nicht dein Item ausgibt.

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
}

Erklärung

  • AttributeValueMemberN.Value ist ein string, kein numerischer Typ, und attributevalue.Marshal lässt das so: int64(9007199254740993) marshallt exakt zu "9007199254740993". DynamoDB-Zahlen tragen 38 Stellen Genauigkeit, mehr als jedes Go-Float — das SDK konvertiert also nie. Das Parsen machst du, am Rand, bewusst.

  • ReturnValues: types.ReturnValueAllNew — der Wert der Konstante ist das Literal "ALL_NEW". types.ReturnValueAllNew.Values() listet alle fünf (NONE, ALL_OLD, UPDATED_OLD, ALL_NEW, UPDATED_NEW), falls du sie lieber am Typ abliest als in den Docs.

  • UpdateExpression ist ein *string, den du selbst baust — oder an das expression-Paket weiter unten abgibst. So oder so ist DynamoDB der einzige Parser: ADD #upd2 :updValue2 ergibt das atomare Inkrement, attribute_exists(Artist) in einer ConditionExpression macht den Aufruf zu einem reinen Update statt zu einem Upsert, und die Klausel-Grammatik steht in Update Expressions.

  • ValidationException hat keinen Go-Typ. types/errors.go definiert 35 Fehler-Structs, darunter ConditionalCheckFailedException, TransactionCanceledException, ProvisionedThroughputExceededException und TransactionConflictException. Validierungsfehler sind nicht dabei — eine fehlerhafte Expression taucht also als generischer smithy.APIError auf, den du nur am String erkennen kannst:

    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

    Beachte das Präfix api error, das die modellierte ConditionalCheckFailedException weiter unten nicht bekommt. Sein Vorhandensein ist ein brauchbares Signal dafür, dass dir errors.As gegen einen konkreten Typ hier nicht weiterhilft.

  • Eine fehlgeschlagene Bedingung ist im Gegensatz dazu ein echter Typ. var ccf *types.ConditionalCheckFailedException; errors.As(err, &ccf) greift, und mit ReturnValuesOnConditionCheckFailure: types.ReturnValuesOnConditionCheckFailureAllOld im Input kommt ccf.Item gefüllt an, mit dem Item so, wie es tatsächlich war. ccf.ErrorMessage() ist das nackte The conditional request failed, ohne die Transport-Präambel, die err.Error() voranstellt.

Das Ergebnis zurücklesen

fmt.Println(out.Attributes) auf einer Interface-Map gibt Adressen aus:

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

Zwei Wege heraus. Unmarshall in ein Struct — das ist, was der meiste Code tun sollte:

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}

Oder assertiere das eine Member, das dich interessiert — und denk daran, dass .Value ein String ist:

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

Lass das expression-Paket sie schreiben

Go ist das einzige SDK auf dieser Site, das die UpdateExpression für dich generiert. feature/dynamodb/expression setzt die Klauseln und beide Maps zusammen:

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

Was herauskommt, ist nicht das, was du geschrieben hast:

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

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

Der Builder hat die Klauseln umsortiert, sie mit einem Zeilenumbruch getrennt und die Platzhalter selbst nummeriert — #0 ist also Awards und nicht der erste Name, den du genannt hast. Nachgelagert stört das niemanden, aber die Strings sind über Änderungen hinweg nicht stabil, was sie zu einer schlechten Grundlage für Assertions in Tests macht. Reich expr.Update(), expr.Names() und expr.Values() direkt in UpdateItemInput und sieh sie dir nie an.

Der Vorteil ist, dass es jeden Namen aliasiert — reservierte Wörter sind damit keine Fehlerklasse mehr, die du ausliefern kannst. Schreibst du die Expression stattdessen von Hand, jag die Attributnamen vorher durch den DynamoDB-Reserved-Words-Checker — die AWS-Liste hat 573 Einträge, und Year, Name und Status stehen alle darauf. Und wenn du ein Item lieber als Daten ansiehst denn als Map von Interface-Pointern, lade DynoTable herunter.

Verwandte Beispiele

Referenzen

Zuletzt verifiziert am 2026-07-28 gegen die oben verlinkte offizielle AWS-Dokumentation.

Mit DynamoDB ohne die Console arbeiten

Ein schneller DynamoDB-Desktop-Client, der das echte SQL ausführt, das DynamoDB nicht kann — JOINs, GROUP BY, Aggregationen — mit visueller Bearbeitung und einem KI-Agenten auf deinen eigenen Bedrock-Schlüsseln.

30 Tage kostenlos testen, keine Kreditkarte — danach der Kostenlos-Tarif ohne Zeitlimit.