DynamoDB UpdateItem en Go (AWS SDK v2)

UpdateItem es sencillo en Go; lo incómodo es que map[string]types.AttributeValue es un mapa de interfaces, así que tanto los valores que envías como los que recibes son punteros a una de nueve structs miembro. Esa única decisión de diseño explica casi toda la fricción de abajo, empezando por el hecho de que el fmt.Println de este fragmento no imprime tu 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
}

Explicación

  • AttributeValueMemberN.Value es un string, no un tipo numérico, y attributevalue.Marshal lo mantiene así: int64(9007199254740993) se marshaliza exactamente como "9007199254740993". Los números de DynamoDB llevan 38 dígitos de precisión, más que cualquier float de Go, así que el SDK nunca convierte. El parseo lo haces tú, en el borde, a propósito.

  • ReturnValues: types.ReturnValueAllNew — el valor de la constante es el literal "ALL_NEW". types.ReturnValueAllNew.Values() lista las cinco (NONE, ALL_OLD, UPDATED_OLD, ALL_NEW, UPDATED_NEW) si prefieres leerlas del tipo en vez de la documentación.

  • UpdateExpression es un *string que construyes tú, o que delegas en el paquete expression de abajo. En cualquier caso DynamoDB es el único que parsea: ADD #upd2 :updValue2 da el incremento atómico, attribute_exists(Artist) en una ConditionExpression hace que la llamada sea solo de actualización en vez de un upsert, y la gramática de las cláusulas está en expresiones de actualización.

  • ValidationException no tiene tipo en Go. types/errors.go define 35 structs de error, incluidos ConditionalCheckFailedException, TransactionCanceledException, ProvisionedThroughputExceededException y TransactionConflictException. Los fallos de validación no están entre ellos, así que una expresión mala aflora como un smithy.APIError genérico que solo puedes reconocer por la cadena:

    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

    Fíjate en el prefijo api error, que la ConditionalCheckFailedException modelada de abajo no lleva. Su presencia es una señal bastante buena de que errors.As contra un tipo concreto no te va a servir aquí.

  • Una condición fallida, en cambio, sí es un tipo real. var ccf *types.ConditionalCheckFailedException; errors.As(err, &ccf) coincide, y con ReturnValuesOnConditionCheckFailure: types.ReturnValuesOnConditionCheckFailureAllOld en la entrada, ccf.Item llega relleno con el Item tal como estaba de verdad. ccf.ErrorMessage() es el escueto The conditional request failed, sin el preámbulo de transporte que err.Error() antepone.

Leer el resultado de vuelta

fmt.Println(out.Attributes) sobre un mapa de interfaces imprime direcciones:

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

Dos salidas. Hacer unmarshal a una struct, que es lo que debería hacer la mayoría del código:

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}

O afirmar el tipo del único miembro que te importa, recordando que .Value es una cadena:

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

Deja que la escriba el paquete expression

Go es el único SDK de este sitio que te genera la UpdateExpression. feature/dynamodb/expression compone las cláusulas y los dos 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()

Lo que sale no es lo que escribiste:

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

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

El builder reordenó las cláusulas, las separó con un salto de línea y numeró él mismo los marcadores, así que #0 es Awards y no el primer nombre que mencionaste. A nadie aguas abajo le importa, pero las cadenas no son estables entre ediciones, lo que las convierte en algo malo sobre lo que aseverar en tests. Pasa expr.Update(), expr.Names() y expr.Values() directos a UpdateItemInput y no los mires nunca.

La ventaja es que aliasea todos los nombres, así que las palabras reservadas dejan de ser una clase de bug que puedes llevarte a producción. Si escribes la expresión a mano, pasa antes los nombres de atributo por el comprobador de palabras reservadas de DynamoDB — la lista de AWS tiene 573 entradas y Year, Name y Status están todas en ella. Y si prefieres mirar un Item como datos en vez de como un mapa de punteros a interfaces, descarga DynoTable.

Ejemplos relacionados

Referencias

Verificado por última vez el 2026-07-28 contra la documentación oficial de AWS enlazada arriba.

Trabaja con DynamoDB sin la Consola

Un cliente de escritorio rápido para DynamoDB que ejecuta el SQL real que DynamoDB no puede — JOINs, GROUP BY, agregaciones — con edición visual y un agente de IA con tus propias claves de Bedrock.

Prueba gratuita de 30 días, sin tarjeta — después, el plan Free sin límite de tiempo.