DynamoDB PutItem in Go (AWS SDK v2)
PutItem writes a whole item and replaces any existing item with the same primary key (item-based actions covers how that differs from UpdateItem). In AWS SDK for Go v2 the interesting part is not the call, it is what your Go values turn into on the way out.
Code
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")
}Explanation
attributevalue.MarshalMap is the shortcut, and it has opinions. Feeding a struct to github.com/aws/aws-sdk-go-v2/feature/dynamodb/attributevalue instead of hand-building the map[string]types.AttributeValue above is the normal move. Here is what it actually produced for a struct with a time.Time, an untouched string field and a nil *int:
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}Three things to take from that. time.Time becomes an RFC 3339 string, not a Unix number, so a timestamp sort key sorts lexicographically and will only behave if every value is zero-padded and in the same zone. An untouched string becomes a real empty-string attribute rather than being left out. And a nil pointer becomes NULL, which is an attribute that exists.
A NULL attribute defeats attribute_not_exists. That is the one that costs an afternoon. Write an item whose Rating came from a nil *int, then guard the next write with attribute_not_exists(Rating) and it fails:
ConditionalCheckFailedException: The conditional request failedDynamoDB is right: the attribute is there, holding NULL. The fix is the struct tag, dynamodbav:"Rating,omitempty", which drops the field instead of nulling it. The same tag stops the empty-string attribute, which matters because those break sparse indexes and, in a key attribute, are rejected outright:
ValidationException: One or more parameter values are not valid. The AttributeValue for a key attribute cannot contain an empty string value. Key: Artisterrors.As, and never string matching. The Go SDK wraps every service error in smithy's operation error, so the string you get from err.Error() is not the message DynamoDB sent:
operation error DynamoDB: PutItem, https response error StatusCode: 400, RequestID: 6886fce0-b246-4762-8a8b-0c6dab813040, ConditionalCheckFailedException: The conditional request failederrors.As(err, &ccf) unwraps it and returned true above; ccf.ErrorMessage() then gives the bare The conditional request failed. A strings.Contains check on the outer string works today and breaks the day the SDK changes its wrapper format.
The typed error can carry the item that blocked you. Set ReturnValuesOnConditionCheckFailure: types.ReturnValuesOnConditionCheckFailureAllOld on the input and ccf.Item comes back populated: five attributes in the run above, including Year as &types.AttributeValueMemberN{Value:"1994"}. That saves the read-after-failure round trip most retry loops do by hand. ReturnValues: types.ReturnValueAllOld is the success-path equivalent (ReturnValues).
Numbers are strings, and that is not a Go quirk. AttributeValueMemberN{Value: "1994"} looks wrong to anyone coming from a typed language, but DynamoDB's N type is a decimal transported as text precisely so nothing has to round-trip through a float64. strconv.FormatInt and strconv.FormatFloat are the conversion; attributevalue does it for you.
What you marshal is what you pay for. A put of a ~15 KB item reported "CapacityUnits": 15 with ReturnConsumedCapacity. Writes round up per 1 KB, so an accidental blob field, or a MarshalMap that emitted attributes you meant to drop, shows up directly on the bill.
Do it visually
Since MarshalMap decides what actually lands in the item, it is worth knowing what that item weighs. The free DynamoDB item size calculator takes the marshalled JSON and returns the byte size and the write units it rounds to.
To write and edit items against your own tables — a form per attribute, type pickers, copy the result back out as Go — download DynoTable.
Related examples
- DynamoDB PutItem in Java — the same conditional write with AWS SDK for Java 2.x, where an unset value fails the opposite way.
- DynamoDB UpdateItem in Go — change specific attributes instead of replacing the item.
- DynamoDB condition expressions —
attribute_not_exists, optimistic locking, and more. - DynamoDB ConditionalCheckFailedException — what the create-only condition throws when the item already exists.
- DynamoDB ValidationException — the catch-all for a malformed item or expression.
References
- PutItem — Amazon DynamoDB API Reference
- Use PutItem with an AWS SDK or CLI — Amazon DynamoDB Developer Guide
- dynamodb package — AWS SDK for Go v2 (pkg.go.dev)
- attributevalue package — AWS SDK for Go v2 (pkg.go.dev)
- Handling errors — AWS SDK for Go v2 Developer Guide
- Condition expressions — Amazon DynamoDB Developer Guide
Reproduced 2026-07-28 on go1.26.5 with aws-sdk-go-v2/service/dynamodb v1.62.1 and feature/dynamodb/attributevalue v1.20.55, against DynamoDB Local (amazon/dynamodb-local) on port 9000. The marshalled values, the error strings and the capacity reading are captured output, copied verbatim.