DynamoDB PutItem en Go (AWS SDK v2)

PutItem escribe un Item entero y reemplaza cualquier Item existente con la misma clave primaria (acciones basadas en Items cubre en qué se diferencia de UpdateItem). En el AWS SDK for Go v2 lo interesante no es la llamada, sino en qué se convierten tus valores de Go de camino a la salida.

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")
}

Explicación

attributevalue.MarshalMap es el atajo, y tiene sus propias ideas. Pasarle una struct a github.com/aws/aws-sdk-go-v2/feature/dynamodb/attributevalue en lugar de construir a mano el map[string]types.AttributeValue de arriba es lo habitual. Esto es lo que produjo de verdad para una struct con un time.Time, un campo string sin tocar y un *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}

Tres cosas que sacar de ahí. time.Time se convierte en una cadena RFC 3339, no en un número Unix, así que una clave de ordenación de marca temporal ordena lexicográficamente y solo se comportará bien si todos los valores llevan relleno de ceros y están en la misma zona horaria. Una cadena sin tocar se convierte en un atributo real de cadena vacía en vez de omitirse. Y un puntero nil se convierte en NULL, que es un atributo que existe.

Un atributo NULL derrota a attribute_not_exists. Ese es el que te cuesta una tarde. Escribe un Item cuyo Rating venga de un *int nil, luego protege la siguiente escritura con attribute_not_exists(Rating) y falla:

ConditionalCheckFailedException: The conditional request failed

DynamoDB tiene razón: el atributo está ahí, con NULL dentro. La solución es la etiqueta de struct, dynamodbav:"Rating,omitempty", que descarta el campo en lugar de ponerlo a null. La misma etiqueta evita el atributo de cadena vacía, lo cual importa porque esos rompen los índices dispersos y, en un atributo de clave, se rechazan de plano:

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, y nunca comparar cadenas. El SDK de Go envuelve cada error de servicio en el error de operación de smithy, así que la cadena que te da err.Error() no es el mensaje que envió DynamoDB:

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

errors.As(err, &ccf) lo desenvuelve y devolvió true arriba; ccf.ErrorMessage() entonces da el escueto The conditional request failed. Una comprobación con strings.Contains sobre la cadena externa funciona hoy y se rompe el día en que el SDK cambie el formato de su envoltorio.

El error tipado puede llevar el Item que te bloqueó. Pon ReturnValuesOnConditionCheckFailure: types.ReturnValuesOnConditionCheckFailureAllOld en la entrada y ccf.Item vuelve relleno: cinco atributos en la ejecución de arriba, incluido Year como &types.AttributeValueMemberN{Value:"1994"}. Eso te ahorra el viaje de lectura tras el fallo que la mayoría de los bucles de reintento hacen a mano. ReturnValues: types.ReturnValueAllOld es el equivalente en el camino de éxito (ReturnValues).

Los números son cadenas, y eso no es una rareza de Go. AttributeValueMemberN{Value: "1994"} le parece mal a cualquiera que venga de un lenguaje tipado, pero el tipo N de DynamoDB es un decimal transportado como texto precisamente para que nada tenga que pasar por un float64 de ida y vuelta. strconv.FormatInt y strconv.FormatFloat son la conversión; attributevalue lo hace por ti.

Pagas por lo que marshalizas. Un put de un Item de unos 15 KB reportó "CapacityUnits": 15 con ReturnConsumedCapacity. Las escrituras se redondean hacia arriba por cada 1 KB, así que un campo blob accidental, o un MarshalMap que emitió atributos que pensabas descartar, aparece directamente en la factura.

Hazlo visualmente

Dado que MarshalMap decide qué acaba realmente en el Item, vale la pena saber cuánto pesa ese Item. La calculadora de tamaño de Item de DynamoDB gratuita toma el JSON marshalled y devuelve el tamaño en bytes y las unidades de escritura a las que redondea.

Para escribir y editar Items contra tus propias tablas — un formulario por atributo, selectores de tipo, copiar el resultado de vuelta como Go — descarga DynoTable.

Ejemplos relacionados

Referencias

Reproducido el 2026-07-28 en go1.26.5 con aws-sdk-go-v2/service/dynamodb v1.62.1 y feature/dynamodb/attributevalue v1.20.55, contra DynamoDB Local (amazon/dynamodb-local) en el puerto 9000. Los valores marshalled, las cadenas de error y la lectura de capacidad son salida capturada, copiada literalmente.

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.