Go言語学習27日目:DynamoDBからGetItemでデータを取得!GET APIの実装トピックを総まとめ!

Go言語学習27日目

Go言語学習スケジュールもついに27日目

あと3日の予定。

今日はDynamoDBに保存されたデータを「GETリクエストで取り出すAPI」を作りつつ、
その実装に必要な技術や考え方を一気に整理しました。

今日のゴール: DynamoDBの GetItem を使ってデータを ID 指定で取得し、 GoのGETエンドポイントからJSONで返す!

昨日はPutItem

kunio-ud-all.com


GetItem APIの基本を理解

DynamoDBのGetItemでは、以下のようにキーを構造体ではなく map 形式で指定します:

input := &dynamodb.GetItemInput{
  TableName: aws.String("Go-Practice-Users"),
  Key: map[string]types.AttributeValue{
    "UserID":  &types.AttributeValueMemberS{Value: "u001"},
    "sortKey": &types.AttributeValueMemberS{Value: "User"},
  },
}

データを構造体に変換:UnmarshalMap

取得結果は map[string]types.AttributeValue の形式で返ってくるので、 attributevalue.UnmarshalMap() を使ってGoの構造体に変換します:

var item Item
err := attributevalue.UnmarshalMap(result.Item, &item)

GETエンドポイントの作成 /items/{id}

Goの標準ライブラリだけでパスパラメータを処理する方法:

id := strings.TrimPrefix(r.URL.Path, "/items/")

この形式ならフレームワーク不要で /items/u001 のようなURLに対応可能!


存在しないIDへの対応

DynamoDBのGetItemは、該当データが存在しない場合でもエラーを返さず、 result.Item == nil になるだけ。

if result.Item == nil {
    http.Error(w, "データが見つかりません", http.StatusNotFound)
    return
}

HTTPレスポンスの整備

w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)

エラー時は意味のあるHTTPステータスを返すことも重要ですね!


全体コード

package main

import (
    "context"
    "encoding/json"
    "fmt"
    "log"
    "net/http"
    "strings"

    "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 Item struct {
    UserID  string `json:"UserID" dynamodbav:"UserID"`
    Name    string `json:"Name" dynamodbav:"Name"`
    Age     int    `json:"Age" dynamodbav:"Age"`
    SortKey string `json:"-" dynamodbav:"sortKey"` // 固定値("User")として保存
}

func main() {
    svc := initDynamoClient()

    http.HandleFunc("/items", func(w http.ResponseWriter, r *http.Request) {
        if r.Method != http.MethodPost {
            http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
            return
        }

        var item Item
        if err := json.NewDecoder(r.Body).Decode(&item); err != nil {
            http.Error(w, "Invalid request body", http.StatusBadRequest)
            return
        }

        if item.UserID == "" || item.Name == "" {
            http.Error(w, "UserIDとNameは必須です", http.StatusBadRequest)
            return
        }

        item.SortKey = "User"

        av, err := attributevalue.MarshalMap(item)
        if err != nil {
            http.Error(w, "Marshal失敗", http.StatusInternalServerError)
            return
        }

        _, err = svc.PutItem(context.TODO(), &dynamodb.PutItemInput{
            TableName: aws.String("Go-Practice-Users"),
            Item:      av,
        })
        if err != nil {
            http.Error(w, "PutItem失敗", http.StatusInternalServerError)
            return
        }

        w.Header().Set("Content-Type", "application/json")
        w.WriteHeader(http.StatusCreated)
        fmt.Fprint(w, `{"message":"Item saved"}`)
    })

    // GET /items/{UserID}
    http.HandleFunc("/items/", func(w http.ResponseWriter, r *http.Request) {
        if r.Method != http.MethodGet {
            http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
            return
        }

        userID := strings.TrimPrefix(r.URL.Path, "/items/")
        if userID == "" {
            http.Error(w, "UserIDが必要です", http.StatusBadRequest)
            return
        }

        input := &dynamodb.GetItemInput{
            TableName: aws.String("Go-Practice-Users"),
            Key: map[string]types.AttributeValue{
                "UserID":  &types.AttributeValueMemberS{Value: userID},
                "sortKey": &types.AttributeValueMemberS{Value: "User"},
            },
        }

        result, err := svc.GetItem(context.TODO(), input)
        if err != nil {
            http.Error(w, "GetItem失敗", http.StatusInternalServerError)
            return
        }

        if result.Item == nil {
            http.Error(w, "データが見つかりません", http.StatusNotFound)
            return
        }

        var item Item
        if err := attributevalue.UnmarshalMap(result.Item, &item); err != nil {
            http.Error(w, "Unmarshal失敗", http.StatusInternalServerError)
            return
        }

        w.Header().Set("Content-Type", "application/json")
        json.NewEncoder(w).Encode(item)
    })

    fmt.Println("🚀 サーバー起動中:http://localhost:8080/items")
    log.Fatal(http.ListenAndServe(":8080", nil))
}

func initDynamoClient() *dynamodb.Client {
    cfg, err := config.LoadDefaultConfig(context.TODO())
    if err != nil {
        log.Fatalf("AWS設定の読み込みに失敗: %v", err)
    }
    return dynamodb.NewFromConfig(cfg)
}

テスト方法(curl)

curl http://localhost:8080/items/u001

返ってくるJSON:

{
  "UserID": "u001",
  "Name": "テスト太郎",
  "Age": 25
}

追加で気になったトピック

  • クエリパラメータ vs パスパラメータの違い(/items?id=xxx との比較)
  • gorilla/muxなどのルーティングライブラリの導入検討

シンプルなアプリでは標準ライブラリで充分ですが、複雑なルートが増えるとライブラリの導入した方が良いですね。 Webフレームワークなど、この一連の流れが終わった後に、やってみようかな。。

Godot?などにもチャレンジしたいですが。。。


以上となります。引き続きよろしくお願いいたします。