Go 中的 DynamoDB Scan(AWS SDK v2)
NewScanPaginator 是一个游标,不是一个集合:它一开始乐观,最后耗尽,而且它的页大小取自一个你眼前这个 ScanInput 并不显示的地方。至于什么时候该彻底避开 Scan,参见 Query vs. Scan。
代码
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)
paginator := dynamodb.NewScanPaginator(client, &dynamodb.ScanInput{
TableName: aws.String("Music"),
FilterExpression: aws.String("#filter0 >= :filterValue0"),
ExpressionAttributeNames: map[string]string{
"#filter0": "Year",
},
ExpressionAttributeValues: map[string]types.AttributeValue{
":filterValue0": &types.AttributeValueMemberN{Value: "2010"},
},
})
var items []map[string]types.AttributeValue
for paginator.HasMorePages() {
page, err := paginator.NextPage(ctx)
if err != nil {
log.Fatalf("scan: %v", err)
}
items = append(items, page.Items...)
}
fmt.Printf("Matched %d items\n", len(items))
}HasMorePages() 实际返回的是什么
针对一份 600 首歌、项目约 3.9 KB 的固定数据,其中有 8 个匹配 Year >= 2010 且排在最后:
fresh HasMorePages(): true (before any request)
page 1: len(page.Items)=0 ScannedCount=271 CU=128.5 LastEvaluatedKey set
page 2: len(page.Items)=0 ScannedCount=271 CU=128.5 LastEvaluatedKey set
page 3: len(page.Items)=8 ScannedCount= 58 CU= 27.5 LastEvaluatedKey nil
exhausted HasMorePages(): false
NextPage() after exhaustion: err = "no more pages available"HasMorePages() 在第一次调用之前是 true,所以这个循环总会至少跑一趟。你不能用它来判断一张表是不是空的。接着有两页在扫描仍在进行时返回了零个项目,这意味着 if len(page.Items) == 0 { break } 会在一张有 8 个匹配项的表上报告空结果。而且这个分页器是一次性的:循环结束后这个值就用完了,NextPage 会返回一个错误而不是重新开始。要再扫一遍,就得再调一次 NewScanPaginator。
Limit 住在两个地方,而选项那个赢
Go 把页大小拆在了输入结构体和分页器自己的选项之间。两边都设,选项优先:
ScanInput.Limit = 500, ScanPaginatorOptions.Limit = 25 -> ScannedCount 25当别处某个函数式选项已经把它设成 25 时,赋值 in.Limit = aws.Int32(500) 还指望拿到 500 项一页,就是一次悄无声息的落空。如果你只碰 ScanInput,分页器是听它的。
2026-07-28 针对 9000 端口上的 DynamoDB Local(amazon/dynamodb-local),使用 go1.26.5 上的 aws-sdk-go-v2/service/dynamodb v1.62.1 实测。
说明
page.Items是[]map[string]types.AttributeValue,不是你的结构体。attributevalue.UnmarshalListOfMaps(items, &songs)一次调用就能转换整批,并把Year变成 Go 的int;映射关系由dynamodbav结构体标签控制。- 数字在传输时是字符串。原始值是
&types.AttributeValueMemberN{Value: "2010"}——N这个成员装的是 Go 的string,所以除非你反序列化进一个带类型的结构体,否则任何算术的两侧都得摆上strconv。 FilterExpression是在读取之后才跑的,这正是那两个空页面照样各花 128.5 个单元的原因。API 参考写得很明白,过滤"does not consume any additional read capacity units",其推论是它也一点都省不下来。#filter0是必需的,不是风格问题。Year是 DynamoDB 的保留字;不做别名就会返回ValidationException: Invalid FilterExpression: Attribute name is a reserved keyword; reserved keyword: Year。Segment/TotalSegments给每个 goroutine 分到表的一片,而每一片都需要自己的分页器。并行削减的是墙上时钟时间,花掉的容量是一样的。
用可视化的方式来做
DynamoDB 查询构建器会输出整个程序的骨架——过滤条件、名称与值映射,以及分页循环——于是本页所测量的那些部分是生成出来的,而不是靠记的。
想在把这次扫描交给某个服务之前,先看看一个过滤条件到底会碰到多少个项目,就下载 DynoTable,在网格里针对这张表跑一遍。
相关示例
- Java 中的 DynamoDB Scan——用 AWS SDK for Java 2.x 做同样的扫描。
- Go 中的 DynamoDB Query——你通常应该优先选择的那种更便宜的读取。
- Query vs. Scan——什么时候(很少)用
Scan才说得过去。 - 为什么我的 DynamoDB Scan 又慢又贵?——成本模型以及如何避开它。
- DynamoDB ProvisionedThroughputExceededException——全表扫描会对一张预置容量表的容量做什么。
- DynamoDB ThrottlingException——另一种限流,以及指数退避如何应对它。