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 は近道であり、独自の流儀を持っています。上のように map[string]types.AttributeValue を手で組み立てる代わりに、構造体を github.com/aws/aws-sdk-go-v2/feature/dynamodb/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}

ここから読み取ることが 3 つあります。time.Time は Unix 数値ではなく RFC 3339 の 文字列 になるので、タイムスタンプのソートキーは辞書順に並びます。すべての値がゼロ埋めされ、同じタイムゾーンにそろっている場合にだけ意図どおりに動きます。触っていない文字列は省略されるのではなく、本物の空文字列属性になります。そして nil ポインターは NULL になり、これは 存在する 属性です。

NULL 属性は attribute_not_exists を無効化します。ここが半日を溶かすところです。nil の *int から来た Rating を持つアイテムを書き込み、次の書き込みを attribute_not_exists(Rating) で守ると、失敗します。

ConditionalCheckFailedException: The conditional request failed

DynamoDB は正しいのです。属性はそこにあり、NULL を保持しています。対策は構造体タグ dynamodbav:"Rating,omitempty" で、これはフィールドを NULL にする代わりに落とします。同じタグは空文字列属性も止めます。空文字列属性はスパースインデックスを壊しますし、キー属性では端的に拒否されるので、これは重要です。

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.ReturnValuesOnConditionCheckFailureAllOld を設定すると ccf.Item が値入りで返ってきます。上の実行では 5 つの属性が入っており、Year&types.AttributeValueMemberN{Value:"1994"} でした。これで、多くのリトライループが手作業でやっている失敗後の読み取りのラウンドトリップを省けます。成功経路での対応物は ReturnValues: types.ReturnValueAllOld です(ReturnValues)。

数値は文字列であり、これは Go の癖ではありませんAttributeValueMemberN{Value: "1994"} は型付き言語から来た人には間違って見えますが、DynamoDB の N 型はテキストで運ばれる 10 進数で、まさに何ものも float64 を経由して往復しなくて済むようにそうなっています。変換は strconv.FormatIntstrconv.FormatFloat です。attributevalue はそれを代わりにやってくれます。

マーシャルしたものが支払うものです。約 15 KB のアイテムの put は ReturnConsumedCapacity"CapacityUnits": 15 を報告しました。書き込みは 1 KB 単位で切り上げられるので、うっかり入れた blob フィールドや、落としたつもりの属性を出してしまった MarshalMap は、そのまま請求書に現れます。

ビジュアルに行う

MarshalMap が実際にアイテムに入るものを決めるので、そのアイテムの重さを知っておく価値があります。無料の DynamoDB アイテムサイズ計算ツールは、マーシャル済みの 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)に対して再現しました。マーシャルされた値、エラー文字列、キャパシティの計測値は、そのまま逐語で写した実出力です。

Console なしで DynamoDB を扱う

DynamoDB では実行できない本物の SQL(JOINs、GROUP BY、集計)を実行する高速な DynamoDB デスクトップクライアント。ビジュアル編集と、あなた自身の Bedrock キーで動く AI エージェントを備えています。

30日間無料トライアル、クレジットカード不要 — その後は期限のない Free プラン。