Most DynamoDB tutorials dump the final code with attributevalue.MarshalMap and expression builders and expect you to understand why. Instead, we’ll start with the raw, ugly way (manually building attribute maps) so when we switch to structs, you’ll actually appreciate what they’re doing for you.
By the end, you’ll have a working Go app that creates a table, writes items, reads them back, queries efficiently, updates, and deletes.
What We’re Building
A deployment tracker that records which services were deployed, when, and by whom. Simple CRUD, but the patterns apply to any DynamoDB project.
The journey:
- Create a table and put an item the raw way
- Hit the verbosity wall and switch to structs
- Get an item back (the ugly way, then the clean way)
- Scan vs Query: learn why Scan is almost always wrong
- Update an item
- Delete an item and clean up
Prerequisites
- Go 1.21+ installed
- AWS CLI configured (
aws sts get-caller-identityshould work) - An AWS account with DynamoDB permissions
Step 1: Create the Table and Put a Raw Item
What: Create a DynamoDB table and insert one item using raw attribute maps.
Why: You need to see the raw SDK types before you can appreciate the shortcuts.
Create your project:
mkdir go-dynamo-crud && cd go-dynamo-crud
go mod init go-dynamo-crud
go get github.com/aws/aws-sdk-go-v2/config
go get github.com/aws/aws-sdk-go-v2/service/dynamodb
go get github.com/aws/aws-sdk-go-v2/feature/dynamodb/attributevalue
main.go
package main
import (
"context"
"fmt"
"time"
"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() {
cfg, err := config.LoadDefaultConfig(context.TODO())
if err != nil {
panic("configuration error, " + err.Error())
}
client := dynamodb.NewFromConfig(cfg)
tableName := "deployments"
// Create the table
_, err = client.CreateTable(context.TODO(), &dynamodb.CreateTableInput{
TableName: aws.String(tableName),
AttributeDefinitions: []types.AttributeDefinition{
{AttributeName: aws.String("ServiceName"), AttributeType: types.ScalarAttributeTypeS},
{AttributeName: aws.String("DeployedAt"), AttributeType: types.ScalarAttributeTypeS},
},
KeySchema: []types.KeySchemaElement{
{AttributeName: aws.String("ServiceName"), KeyType: types.KeyTypeHash},
{AttributeName: aws.String("DeployedAt"), KeyType: types.KeyTypeRange},
},
BillingMode: types.BillingModePayPerRequest,
})
if err != nil {
panic("failed to create table: " + err.Error())
}
// Wait for the table to exist
waiter := dynamodb.NewTableExistsWaiter(client)
err = waiter.Wait(context.TODO(), &dynamodb.DescribeTableInput{
TableName: aws.String(tableName),
}, 2*time.Minute)
if err != nil {
panic("table did not become active: " + err.Error())
}
fmt.Println("table created:", tableName)
// Put an item — the raw way
_, err = client.PutItem(context.TODO(), &dynamodb.PutItemInput{
TableName: aws.String(tableName),
Item: map[string]types.AttributeValue{
"ServiceName": &types.AttributeValueMemberS{Value: "auth-api"},
"DeployedAt": &types.AttributeValueMemberS{Value: "2026-02-14T10:30:00Z"},
"Version": &types.AttributeValueMemberS{Value: "v1.4.2"},
"Status": &types.AttributeValueMemberS{Value: "success"},
"DeployedBy": &types.AttributeValueMemberS{Value: "karandeep"},
},
})
if err != nil {
panic("failed to put item: " + err.Error())
}
fmt.Println("item inserted: auth-api v1.4.2")
}
We have two key schema elements: ServiceName as the partition key (HASH) and DeployedAt as the sort key (RANGE). This lets us store multiple deployments per service, sorted by time. BillingModePayPerRequest means no capacity planning. You pay per read/write.
The NewTableExistsWaiter polls DynamoDB until the table is active. Without this, your PutItem would fail because the table isn’t ready yet.
Run it:
go run main.go
Expected output:
table created: deployments
item inserted: auth-api v1.4.2
It works. But look at that PutItem call: five fields, each wrapped in &types.AttributeValueMemberS{Value: "..."}. Now imagine adding three more items the same way. That’s 20 lines of AttributeValueMemberS boilerplate. Let’s see how bad it gets.
Step 2: Switch to Structs
What: Use attributevalue.MarshalMap to convert Go structs to DynamoDB items.
Why: The raw attribute map approach is verbose and error-prone. Misspell a field name in the map and DynamoDB won’t complain. It’ll just store the wrong key. Structs with dynamodbav tags fix both problems.
Replace your entire main.go:
main.go
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/feature/dynamodb/attributevalue"
"github.com/aws/aws-sdk-go-v2/service/dynamodb"
)
type Deployment struct {
ServiceName string `dynamodbav:"ServiceName"`
DeployedAt string `dynamodbav:"DeployedAt"`
Version string `dynamodbav:"Version"`
Status string `dynamodbav:"Status"`
DeployedBy string `dynamodbav:"DeployedBy"`
}
func main() {
cfg, err := config.LoadDefaultConfig(context.TODO())
if err != nil {
panic("configuration error, " + err.Error())
}
client := dynamodb.NewFromConfig(cfg)
// Table already exists from Step 1 — just insert items
deploys := []Deployment{
{ServiceName: "auth-api", DeployedAt: "2026-02-14T14:00:00Z", Version: "v1.4.3", Status: "failed", DeployedBy: "karandeep"},
{ServiceName: "payment-svc", DeployedAt: "2026-02-14T09:00:00Z", Version: "v2.1.0", Status: "success", DeployedBy: "karandeep"},
{ServiceName: "payment-svc", DeployedAt: "2026-02-14T16:00:00Z", Version: "v2.1.1", Status: "success", DeployedBy: "karandeep"},
}
for _, d := range deploys {
item, err := attributevalue.MarshalMap(d)
if err != nil {
log.Fatalf("failed to marshal %s: %v", d.ServiceName, err)
}
_, err = client.PutItem(context.TODO(), &dynamodb.PutItemInput{
TableName: aws.String("deployments"),
Item: item,
})
if err != nil {
log.Fatalf("failed to put %s: %v", d.ServiceName, err)
}
fmt.Printf("inserted: %s %s (%s)\n", d.ServiceName, d.Version, d.Status)
}
fmt.Printf("done — %d items inserted\n", len(deploys))
}
The dynamodbav struct tag tells attributevalue.MarshalMap how to map each Go field to a DynamoDB attribute name. The struct becomes the single source of truth for your data shape.
Compare the two approaches:
// Raw — verbose, error-prone, no compile-time checks
"ServiceName": &types.AttributeValueMemberS{Value: "auth-api"},
"DeployedAt": &types.AttributeValueMemberS{Value: "2026-02-14T14:00:00Z"},
// Struct — fill in fields, marshalling is automatic
Deployment{ServiceName: "auth-api", DeployedAt: "2026-02-14T14:00:00Z"}
Run it:
go run main.go
Expected output:
inserted: auth-api v1.4.3 (failed)
inserted: payment-svc v2.1.0 (success)
inserted: payment-svc v2.1.1 (success)
done — 3 items inserted
The table now has 4 items total (1 from Step 1 + 3 from here). Now let’s read them back.
Step 3: Get an Item Back
What: Retrieve a single item by its key.
Why: DynamoDB’s GetItem requires the full primary key, both the partition key and the sort key. If you only give the partition key, you get an error. This trips up everyone coming from SQL databases where you query by any column.
Replace your entire main.go:
main.go
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/feature/dynamodb/attributevalue"
"github.com/aws/aws-sdk-go-v2/service/dynamodb"
"github.com/aws/aws-sdk-go-v2/service/dynamodb/types"
)
type Deployment struct {
ServiceName string `dynamodbav:"ServiceName"`
DeployedAt string `dynamodbav:"DeployedAt"`
Version string `dynamodbav:"Version"`
Status string `dynamodbav:"Status"`
DeployedBy string `dynamodbav:"DeployedBy"`
}
func main() {
cfg, err := config.LoadDefaultConfig(context.TODO())
if err != nil {
panic("configuration error, " + err.Error())
}
client := dynamodb.NewFromConfig(cfg)
// Get an item — you need BOTH the partition key AND the sort key
result, err := client.GetItem(context.TODO(), &dynamodb.GetItemInput{
TableName: aws.String("deployments"),
Key: map[string]types.AttributeValue{
"ServiceName": &types.AttributeValueMemberS{Value: "auth-api"},
"DeployedAt": &types.AttributeValueMemberS{Value: "2026-02-14T10:30:00Z"},
},
})
if err != nil {
log.Fatal("failed to get item:", err)
}
// The RAW way — type-switch on every attribute
fmt.Println("=== Raw approach ===")
for key, attr := range result.Item {
switch v := attr.(type) {
case *types.AttributeValueMemberS:
fmt.Printf(" %s: %s\n", key, v.Value)
case *types.AttributeValueMemberN:
fmt.Printf(" %s: %s\n", key, v.Value)
default:
fmt.Printf(" %s: (unknown type)\n", key)
}
}
// The CLEAN way — UnmarshalMap into a struct
fmt.Println("\n=== Struct approach ===")
var deploy Deployment
err = attributevalue.UnmarshalMap(result.Item, &deploy)
if err != nil {
log.Fatal("failed to unmarshal:", err)
}
fmt.Printf(" Service: %s\n", deploy.ServiceName)
fmt.Printf(" Version: %s\n", deploy.Version)
fmt.Printf(" Status: %s\n", deploy.Status)
fmt.Printf(" Deployed: %s by %s\n", deploy.DeployedAt, deploy.DeployedBy)
}
Both approaches use the same GetItem call and the same response. The difference is how you read the result. The raw way requires a type-switch for every attribute type (AttributeValueMemberS, AttributeValueMemberN, AttributeValueMemberBOOL…). For five fields it’s annoying. For fifteen it’s a nightmare.
attributevalue.UnmarshalMap is the mirror of MarshalMap. It reads the dynamodbav tags on your struct and maps the DynamoDB attributes back into Go fields. No type-switching needed.
Run it:
go run main.go
Expected output:
=== Raw approach ===
ServiceName: auth-api
Status: success
DeployedBy: karandeep
DeployedAt: 2026-02-14T10:30:00Z
Version: v1.4.2
=== Struct approach ===
Service: auth-api
Version: v1.4.2
Status: success
Deployed: 2026-02-14T10:30:00Z by karandeep
Note the raw output comes in random order. DynamoDB maps are unordered. The struct approach gives you named fields, so you control the display.
Step 4: Scan vs Query
What: Find multiple items, first with Scan (reads everything), then with Query (reads only what matches).
Why: This is the single most important DynamoDB concept. Scan reads every item in the table and filters after. Query uses the partition key to read only matching items. On a table with a million rows, Scan costs you real money. Query doesn’t.
Replace your entire main.go:
main.go
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/feature/dynamodb/attributevalue"
"github.com/aws/aws-sdk-go-v2/service/dynamodb"
"github.com/aws/aws-sdk-go-v2/service/dynamodb/types"
)
type Deployment struct {
ServiceName string `dynamodbav:"ServiceName"`
DeployedAt string `dynamodbav:"DeployedAt"`
Version string `dynamodbav:"Version"`
Status string `dynamodbav:"Status"`
DeployedBy string `dynamodbav:"DeployedBy"`
}
func main() {
cfg, err := config.LoadDefaultConfig(context.TODO())
if err != nil {
panic("configuration error, " + err.Error())
}
client := dynamodb.NewFromConfig(cfg)
// SCAN — find all failed deployments (reads the ENTIRE table)
fmt.Println("=== Scan: find failed deployments ===")
scanResult, err := client.Scan(context.TODO(), &dynamodb.ScanInput{
TableName: aws.String("deployments"),
FilterExpression: aws.String("#s = :status"),
ExpressionAttributeNames: map[string]string{
"#s": "Status",
},
ExpressionAttributeValues: map[string]types.AttributeValue{
":status": &types.AttributeValueMemberS{Value: "failed"},
},
})
if err != nil {
log.Fatal("scan failed:", err)
}
var failed []Deployment
attributevalue.UnmarshalListOfMaps(scanResult.Items, &failed)
fmt.Printf("scanned %d items, found %d failures:\n", scanResult.ScannedCount, len(failed))
for _, d := range failed {
fmt.Printf(" FAILED: %s %s at %s\n", d.ServiceName, d.Version, d.DeployedAt)
}
// QUERY — get all deployments for auth-api (reads ONLY that partition)
fmt.Println("\n=== Query: auth-api deployments ===")
queryResult, err := client.Query(context.TODO(), &dynamodb.QueryInput{
TableName: aws.String("deployments"),
KeyConditionExpression: aws.String("ServiceName = :svc"),
ExpressionAttributeValues: map[string]types.AttributeValue{
":svc": &types.AttributeValueMemberS{Value: "auth-api"},
},
})
if err != nil {
log.Fatal("query failed:", err)
}
var authDeploys []Deployment
attributevalue.UnmarshalListOfMaps(queryResult.Items, &authDeploys)
fmt.Printf("found %d deployments for auth-api:\n", len(authDeploys))
for _, d := range authDeploys {
fmt.Printf(" %s %s %s\n", d.DeployedAt, d.Version, d.Status)
}
}
A few things to unpack here.
Scan: Status is a reserved word in DynamoDB, so you can’t use it directly in expressions. That’s what #s is for. ExpressionAttributeNames replaces #s with the actual attribute name. The ExpressionAttributeValues map replaces :status with the value. DynamoDB requires this placeholder pattern for all expressions.
Query: Uses KeyConditionExpression instead of FilterExpression. This tells DynamoDB to only read items where ServiceName equals auth-api. It doesn’t touch other partitions at all. ServiceName isn’t a reserved word, so we don’t need ExpressionAttributeNames here.
UnmarshalListOfMaps works like UnmarshalMap but for a slice of items.
Run it:
go run main.go
Expected output:
=== Scan: find failed deployments ===
scanned 4 items, found 1 failures:
FAILED: auth-api v1.4.3 at 2026-02-14T14:00:00Z
=== Query: auth-api deployments ===
found 2 deployments for auth-api:
2026-02-14T10:30:00Z v1.4.2 success
2026-02-14T14:00:00Z v1.4.3 failed
Notice the scan read all 4 items to find 1 match. The query only read the 2 items in the auth-api partition. On a table with 10 million items, that difference is your AWS bill.
Also notice that Query results come back sorted by the sort key (DeployedAt) automatically. That’s why we chose a timestamp as the sort key: chronological order for free.
The rule: Use Query when you know the partition key. Use Scan only when you genuinely need to search across all partitions (rare in production).
Step 5: Update an Item
What: Change a field on an existing item without replacing the whole thing.
Why: The failed auth-api v1.4.3 deploy got fixed. We want to update its status from “failed” to “success” without rewriting all five fields.
Replace your entire main.go:
main.go
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/feature/dynamodb/attributevalue"
"github.com/aws/aws-sdk-go-v2/service/dynamodb"
"github.com/aws/aws-sdk-go-v2/service/dynamodb/types"
)
type Deployment struct {
ServiceName string `dynamodbav:"ServiceName"`
DeployedAt string `dynamodbav:"DeployedAt"`
Version string `dynamodbav:"Version"`
Status string `dynamodbav:"Status"`
DeployedBy string `dynamodbav:"DeployedBy"`
}
func main() {
cfg, err := config.LoadDefaultConfig(context.TODO())
if err != nil {
panic("configuration error, " + err.Error())
}
client := dynamodb.NewFromConfig(cfg)
serviceName := "auth-api"
deployedAt := "2026-02-14T14:00:00Z"
// First, show the current state
fmt.Println("=== Before update ===")
before := getItem(client, serviceName, deployedAt)
fmt.Printf(" %s %s — status: %s\n", before.ServiceName, before.Version, before.Status)
// Update just the Status field
_, err = client.UpdateItem(context.TODO(), &dynamodb.UpdateItemInput{
TableName: aws.String("deployments"),
Key: map[string]types.AttributeValue{
"ServiceName": &types.AttributeValueMemberS{Value: serviceName},
"DeployedAt": &types.AttributeValueMemberS{Value: deployedAt},
},
UpdateExpression: aws.String("SET #s = :status"),
ExpressionAttributeNames: map[string]string{
"#s": "Status",
},
ExpressionAttributeValues: map[string]types.AttributeValue{
":status": &types.AttributeValueMemberS{Value: "success"},
},
})
if err != nil {
log.Fatal("update failed:", err)
}
fmt.Println("\nstatus updated to 'success'")
// Verify the update
fmt.Println("\n=== After update ===")
after := getItem(client, serviceName, deployedAt)
fmt.Printf(" %s %s — status: %s\n", after.ServiceName, after.Version, after.Status)
}
func getItem(client *dynamodb.Client, serviceName, deployedAt string) Deployment {
result, err := client.GetItem(context.TODO(), &dynamodb.GetItemInput{
TableName: aws.String("deployments"),
Key: map[string]types.AttributeValue{
"ServiceName": &types.AttributeValueMemberS{Value: serviceName},
"DeployedAt": &types.AttributeValueMemberS{Value: deployedAt},
},
})
if err != nil {
log.Fatal("get failed:", err)
}
var deploy Deployment
attributevalue.UnmarshalMap(result.Item, &deploy)
return deploy
}
The UpdateExpression uses SET to change specific attributes. You always need the full primary key (both partition and sort key) to identify which item to update. You can set multiple fields at once: SET #s = :status, Version = :ver.
Run it:
go run main.go
Expected output:
=== Before update ===
auth-api v1.4.3 — status: failed
status updated to 'success'
=== After update ===
auth-api v1.4.3 — status: success
The update only changed the Status field. The other four fields (ServiceName, DeployedAt, Version, DeployedBy) are untouched.
Step 6: Delete an Item and Clean Up
What: Remove an item from the table, then delete the table itself.
Why: Old deployments need cleanup. Delete works like GetItem: you provide the full primary key. And when we’re done experimenting, we delete the table so we’re not paying for it.
Replace your entire main.go:
main.go
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/feature/dynamodb/attributevalue"
"github.com/aws/aws-sdk-go-v2/service/dynamodb"
"github.com/aws/aws-sdk-go-v2/service/dynamodb/types"
)
type Deployment struct {
ServiceName string `dynamodbav:"ServiceName"`
DeployedAt string `dynamodbav:"DeployedAt"`
Version string `dynamodbav:"Version"`
Status string `dynamodbav:"Status"`
DeployedBy string `dynamodbav:"DeployedBy"`
}
func main() {
cfg, err := config.LoadDefaultConfig(context.TODO())
if err != nil {
panic("configuration error, " + err.Error())
}
client := dynamodb.NewFromConfig(cfg)
// Show what we have before deleting
fmt.Println("=== Before delete ===")
authDeploys := queryService(client, "auth-api")
fmt.Printf("auth-api has %d deployments\n", len(authDeploys))
for _, d := range authDeploys {
fmt.Printf(" %s %s %s\n", d.DeployedAt, d.Version, d.Status)
}
// Delete the oldest auth-api deployment
_, err = client.DeleteItem(context.TODO(), &dynamodb.DeleteItemInput{
TableName: aws.String("deployments"),
Key: map[string]types.AttributeValue{
"ServiceName": &types.AttributeValueMemberS{Value: "auth-api"},
"DeployedAt": &types.AttributeValueMemberS{Value: "2026-02-14T10:30:00Z"},
},
})
if err != nil {
log.Fatal("delete failed:", err)
}
fmt.Println("\ndeleted auth-api deploy from 10:30")
// Verify it's gone
fmt.Println("\n=== After delete ===")
authDeploys = queryService(client, "auth-api")
fmt.Printf("auth-api now has %d deployments\n", len(authDeploys))
for _, d := range authDeploys {
fmt.Printf(" %s %s %s\n", d.DeployedAt, d.Version, d.Status)
}
// Clean up — delete the table
_, err = client.DeleteTable(context.TODO(), &dynamodb.DeleteTableInput{
TableName: aws.String("deployments"),
})
if err != nil {
log.Fatal("failed to delete table:", err)
}
fmt.Println("\ntable 'deployments' deleted — cleanup complete")
}
func queryService(client *dynamodb.Client, serviceName string) []Deployment {
result, err := client.Query(context.TODO(), &dynamodb.QueryInput{
TableName: aws.String("deployments"),
KeyConditionExpression: aws.String("ServiceName = :svc"),
ExpressionAttributeValues: map[string]types.AttributeValue{
":svc": &types.AttributeValueMemberS{Value: serviceName},
},
})
if err != nil {
log.Fatal("query failed:", err)
}
var deploys []Deployment
attributevalue.UnmarshalListOfMaps(result.Items, &deploys)
return deploys
}
Run it:
go run main.go
Expected output:
=== Before delete ===
auth-api has 2 deployments
2026-02-14T10:30:00Z v1.4.2 success
2026-02-14T14:00:00Z v1.4.3 success
deleted auth-api deploy from 10:30
=== After delete ===
auth-api now has 1 deployments
2026-02-14T14:00:00Z v1.4.3 success
table 'deployments' deleted — cleanup complete
No confirmation prompt, no soft delete. The item is gone immediately. DynamoDB doesn’t have a recycle bin. If you need to recover deleted items in production, enable Point-in-Time Recovery on the table before deleting anything.
What We Built
Starting from raw attribute maps that nobody wants to maintain, we incrementally built:
- Table creation: composite key (partition + sort) with a waiter
- PutItem with structs:
MarshalMapconverts Go structs to DynamoDB items - GetItem with structs:
UnmarshalMapconverts DynamoDB items back to Go structs - Scan vs Query: why Query is almost always the right choice
- UpdateItem: change specific fields with
SETexpressions - DeleteItem: remove by primary key
The pattern for every DynamoDB operation is the same: load config, create client, build input struct, call the method, unmarshal the result. Once you’ve seen it for PutItem, you’ve seen it for everything.
Next Steps
This covers single-table CRUD. In a real project, you’d add:
- Batch writes with
BatchWriteItemfor bulk inserts - Pagination for large Query results (check
result.LastEvaluatedKey) - Conditional writes to prevent overwriting existing items
- GSIs (Global Secondary Indexes) to query by non-key attributes like
Status
Check out Building a Go Lambda Function to put this DynamoDB code behind an API endpoint, Building a Go S3 CLI Tool for more Go + AWS SDK patterns, or Terraform From Scratch to provision DynamoDB tables as code.
Cheat Sheet
Copy-paste reference for when you just need the syntax.
Setup:
cfg, _ := config.LoadDefaultConfig(context.TODO())
client := dynamodb.NewFromConfig(cfg)
Struct with tags:
type Item struct {
PK string `dynamodbav:"PK"`
SK string `dynamodbav:"SK"`
Name string `dynamodbav:"Name"`
}
Put item:
item, _ := attributevalue.MarshalMap(myStruct)
client.PutItem(ctx, &dynamodb.PutItemInput{TableName: aws.String("table"), Item: item})
Get item:
result, _ := client.GetItem(ctx, &dynamodb.GetItemInput{
TableName: aws.String("table"),
Key: map[string]types.AttributeValue{
"PK": &types.AttributeValueMemberS{Value: "value"},
},
})
var item Item
attributevalue.UnmarshalMap(result.Item, &item)
Query by partition key:
result, _ := client.Query(ctx, &dynamodb.QueryInput{
TableName: aws.String("table"),
KeyConditionExpression: aws.String("PK = :pk"),
ExpressionAttributeValues: map[string]types.AttributeValue{
":pk": &types.AttributeValueMemberS{Value: "value"},
},
})
var items []Item
attributevalue.UnmarshalListOfMaps(result.Items, &items)
Update one field:
client.UpdateItem(ctx, &dynamodb.UpdateItemInput{
TableName: aws.String("table"),
Key: key, // same format as GetItem
UpdateExpression: aws.String("SET #f = :val"),
ExpressionAttributeNames: map[string]string{"#f": "FieldName"},
ExpressionAttributeValues: map[string]types.AttributeValue{":val": &types.AttributeValueMemberS{Value: "new"}},
})
Delete item:
client.DeleteItem(ctx, &dynamodb.DeleteItemInput{TableName: aws.String("table"), Key: key})
Key rules to remember:
GetItem,UpdateItem,DeleteItemall need the full primary key (partition + sort if you have one)Queryneeds at least the partition key. Use it when you know which partition to look inScanreads the entire table. Avoid in production, use only for debugging or small tablesStatus,Name,Size,Countare reserved words. Use#placeholderin expressionsMarshalMapturns a Go struct into DynamoDB format,UnmarshalMapdoes the reverse- Always use
PayPerRequestbilling for dev tables. No capacity planning needed