Go 中的 DynamoDB UpdateItem(AWS SDK v2)
UpdateItem 在 Go 里很直白;别扭的地方在于 map[string]types.AttributeValue 是一张_接口_的映射,所以你发出去的值和拿回来的值都是指向九个成员结构体之一的指针。这一个设计选择解释了下面绝大部分的摩擦,先从这一条说起:本例里那个 fmt.Println 并不会打印你的项目。
代码
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
}说明
AttributeValueMemberN.Value是一个string,不是数值类型,而attributevalue.Marshal会保持这一点:int64(9007199254740993)会精确地 marshal 成"9007199254740993"。DynamoDB 的数字带 38 位精度,比任何 Go 浮点数都多,所以 SDK 从不做转换。解析由你在边界处、有意识地完成。ReturnValues: types.ReturnValueAllNew——这个常量的值就是字面量"ALL_NEW"。如果你宁愿从类型上读而不是从文档上读,types.ReturnValueAllNew.Values()会列出全部五个(NONE、ALL_OLD、UPDATED_OLD、ALL_NEW、UPDATED_NEW)。UpdateExpression是一个你自己拼的*string,或者交给下面那个expression包去拼。不管哪种,DynamoDB 都是唯一的解析者:ADD #upd2 :updValue2给出原子自增,在ConditionExpression里写attribute_exists(Artist)会让这次调用变成只更新而不是 upsert,而子句语法在更新表达式里。ValidationException没有对应的 Go 类型。types/errors.go定义了 35 个错误结构体,包括ConditionalCheckFailedException、TransactionCanceledException、ProvisionedThroughputExceededException和TransactionConflictException。校验失败不在其中,所以一个坏表达式浮现出来的是一个泛化的smithy.APIError,你只能靠字符串来认它: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注意那个
api error前缀,下面那个已建模的ConditionalCheckFailedException是没有的。它的出现是个不错的信号:在这里对着一个具体类型做errors.As帮不了你。相比之下,条件失败是有真类型的。
var ccf *types.ConditionalCheckFailedException; errors.As(err, &ccf)能匹配上,而且在输入上带ReturnValuesOnConditionCheckFailure: types.ReturnValuesOnConditionCheckFailureAllOld时,ccf.Item回来时装着项目当时真实的样子。ccf.ErrorMessage()是光秃秃的The conditional request failed,没有err.Error()会加在前面的那段传输层前言。
把结果读回来
对一张接口映射调用 fmt.Println(out.Attributes) 打印出来的是地址:
map[Artist:0x1c8cf589e060 Awards:0x1c8cf589e078 Genre:0x1c8cf589e090 SongTitle:0x1c8cf589e0c0 Year:0x1c8cf589e0a8]有两条出路。反序列化进一个结构体,这是大多数代码该做的:
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}或者对你关心的那一个成员做类型断言,并记住 .Value 是字符串:
n, ok := out.Attributes["Awards"].(*types.AttributeValueMemberN)
if ok {
awards, _ := strconv.Atoi(n.Value) // "1" -> 1
fmt.Println(awards)
}让 expression 包替你写
Go 是本站唯一一个会替你生成 UpdateExpression 的 SDK。feature/dynamodb/expression 会把子句和两张映射一起拼出来:
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()出来的东西并不是你写的那样:
UpdateExpression: ADD #0 :0
SET #1 = :1, #2 = :2
Names: map[#0:Awards #1:Genre #2:Year]构建器重排了子句、用换行把它们分开,并自己给占位符编了号,于是 #0 是 Awards,而不是你先提到的那个名字。下游没人在意这些,但这些字符串在改动之间并不稳定,因此拿它们在测试里做断言并不合适。把 expr.Update()、expr.Names() 和 expr.Values() 直接塞进 UpdateItemInput,然后再也别看它们。
好处是它会给每一个名字做别名,于是保留字不再是一类你可能带着上线的 bug。如果你选择手写表达式,先把属性名过一遍 DynamoDB 保留字检查器——AWS 那份清单有 573 项,而 Year、Name 和 Status 全都在上面。而如果你宁愿把项目当作数据来看,而不是当作一张接口指针的映射,就下载 DynoTable。
相关示例
- Java 中的 DynamoDB UpdateItem——用 AWS SDK for Java 2.x 做同样的更新。
- Go 中的 DynamoDB PutItem——改为替换整个项目。
- DynamoDB 更新表达式——
SET、ADD、REMOVE、DELETE以及惯用写法。 - 理解 ReturnValues——每个
ReturnValues选项各给你什么。 - "Attribute name is a reserved keyword"——为什么这里的别名映射不是可选的。
- "Invalid UpdateExpression" 语法错误——常见的 SET/ADD 语法错误的解读。
参考资料
- UpdateItem — Amazon DynamoDB API Reference
- Use UpdateItem with an AWS SDK or CLI — Amazon DynamoDB Developer Guide
- dynamodb package — AWS SDK for Go v2 (pkg.go.dev)
- dynamodb/types package — AWS SDK for Go v2 (pkg.go.dev)
- expression package — AWS SDK for Go v2 (pkg.go.dev)
- Update expressions — Amazon DynamoDB Developer Guide
最后核实于 2026-07-28,依据上方链接的 AWS 官方文档。