Go 中的 DynamoDB PutItem(AWS SDK v2)

PutItem 写入一个完整的项目,并替换掉任何具有相同主键的已有项目(基于项目的操作讲了它与 UpdateItem 的区别)。在 AWS SDK for Go v2 里,有意思的不是这个调用本身,而是你的 Go 值在出去的路上变成了什么。

代码

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

说明

attributevalue.MarshalMap 是那条捷径,而且它有自己的主张。把一个结构体喂给 github.com/aws/aws-sdk-go-v2/feature/dynamodb/attributevalue,而不是像上面那样手工拼 map[string]types.AttributeValue,是常规做法。下面是它对一个含 time.Time、一个没赋值的 string 字段和一个 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}

从中能拿到三件事。time.Time 变成的是一个 RFC 3339 字符串,不是 Unix 数字,所以用时间戳做排序键是按字典序排的,只有在每个值都零填充、且处于同一时区时才会表现正常。一个没赋值的字符串会变成一个真实存在的空字符串属性,而不是被略去。而 nil 指针会变成 NULL,那是一个存在的属性。

一个 NULL 属性会让 attribute_not_exists 失效。这就是要搭上一个下午的那种坑。写入一个 Rating 来自 nil *int 的项目,然后用 attribute_not_exists(Rating) 去守卫下一次写入,它会失败:

ConditionalCheckFailedException: The conditional request failed

DynamoDB 没错:那个属性确实在,装着 NULL。修法是那个结构体标签 dynamodbav:"Rating,omitempty",它会丢掉这个字段而不是把它置空。同一个标签也能挡住空字符串属性,而这很要紧,因为空字符串会破坏稀疏索引,而且出现在键属性上会被直接拒绝:

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,绝不做字符串匹配。Go SDK 会把每个服务端错误包进 smithy 的操作错误里,所以你从 err.Error() 拿到的字符串并不是 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) 会把它解包,上面那次运行返回了 true;接着 ccf.ErrorMessage() 给出光秃秃的 The conditional request failed。在外层字符串上做 strings.Contains 检查今天能用,等 SDK 哪天改了包装格式就坏了。

带类型的错误可以捎上挡住你的那个项目。在输入上设置 ReturnValuesOnConditionCheckFailure: types.ReturnValuesOnConditionCheckFailureAllOldccf.Item 回来时就是有内容的:上面那次运行里有五个属性,其中 Year&types.AttributeValueMemberN{Value:"1994"}。这省掉了大多数重试循环手工做的"失败后再读一次"的往返。ReturnValues: types.ReturnValueAllOld 是成功路径上的对应物(ReturnValues)。

数字是字符串,而这不是 Go 的怪癖AttributeValueMemberN{Value: "1994"} 在从强类型语言过来的人看来是错的,但 DynamoDB 的 N 类型本就是以文本传输的十进制数,正是为了让任何东西都不必经由 float64 往返一趟。转换用 strconv.FormatIntstrconv.FormatFloatattributevalue 会替你做。

你 marshal 了什么,就为什么付钱。写入一个约 15 KB 的项目,在 ReturnConsumedCapacity 下报告 "CapacityUnits": 15。写入按每 1 KB 向上取整,所以一个不小心带上的大字段,或者一个把你本想丢掉的属性也发出去的 MarshalMap,都会直接体现在账单上。

用可视化的方式来做

既然是 MarshalMap 决定了最终落进项目里的东西,那就值得知道这个项目有多重。免费的 DynamoDB 项目大小计算器接收 marshal 后的 JSON,返回字节大小以及它向上取整到的写入单元数。

想针对自己的表写入和编辑项目——每个属性一个表单、带类型选择器、把结果作为 Go 代码复制出来——就下载 DynoTable

相关示例

参考资料

2026-07-28 在 go1.26.5 上、使用 aws-sdk-go-v2/service/dynamodb v1.62.1 与 feature/dynamodb/attributevalue v1.20.55、针对 9000 端口上的 DynamoDB Local(amazon/dynamodb-local)复现。marshal 后的值、错误字符串与容量读数均为捕获到的输出,原样照录。

无需控制台即可使用 DynamoDB

一款快速的 DynamoDB 桌面客户端,可运行 DynamoDB 无法执行的真正 SQL——JOINs、GROUP BY、聚合——并支持可视化编辑和运行在你自己的 Bedrock 密钥上的 AI agent。

30 天免费试用,无需信用卡 — 之后为无时间限制的免费版。