Go言語学習28日目:APIらしいコード設計に挑戦!関数分割・責務整理・エラーハンドリング改善まとめ

Go言語学習28日目

Go言語学習もついに28日目
30分続けてきたものもあと、2日 (時より休むこともありましたが、、、)

ここまで学んできたPOST・GET APIを実用的な設計に近づけるためのコード整理を行いました。

kunio-ud-all.com

今日のテーマ
  • 処理の関数分割
  • ログ出力の明確化
  • エラー処理の一元化
  • HTTPレスポンスの統一

🔧 整理したポイントまとめ

1. 関数分割:ハンドラ・ロジック・ユーティリティを明確に分離
// ハンドラ関数(ルーティング)
// ビジネスロジック(Put/Get)
// DynamoDBクライアント初期化

これにより、各関数の責務が明確になって、読みやすく、保守しやすくなりました!


2. エラーハンドリング:一貫したレスポンス形式を導入

共通のエラー応答関数を用意:

func respondError(w http.ResponseWriter, code int, msg string) {
    w.Header().Set("Content-Type", "application/json")
    w.WriteHeader(code)
    json.NewEncoder(w).Encode(map[string]string{"error": msg})
}

どのエラーでも JSON で返るように統一しました。


3. ログ出力:エラーや処理開始・終了を明示
log.Printf("📥 受信: POST /items")
log.Printf("⚠ エラー: %v", err)

処理の流れをログで追えるようになり、デバッグしやすさが段違いになりました!


4. HTTPレスポンスの改善:ステータスコードも意識
  • 登録成功 → 201 Created
  • データなし → 404 Not Found
  • リクエスト不備 → 400 Bad Request
  • 内部エラー → 500 Internal Server Error

💻 改良後のコード

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

var svc *dynamodb.Client

func main() {
    svc = initDynamoClient()

    http.HandleFunc("/items", handlePostItem)
    http.HandleFunc("/items/", handleGetItem)

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

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

    log.Println("📥 POST /items 受信")

    var item Item
    if err := json.NewDecoder(r.Body).Decode(&item); err != nil {
        respondError(w, http.StatusBadRequest, "リクエスト形式が不正です")
        return
    }

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

    item.SortKey = "User"

    if err := putItemToDynamo(item); err != nil {
        respondError(w, http.StatusInternalServerError, "データ登録に失敗しました")
        return
    }

    w.WriteHeader(http.StatusCreated)
    json.NewEncoder(w).Encode(map[string]string{"message": "登録成功"})
}

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

    id := strings.TrimPrefix(r.URL.Path, "/items/")
    if id == "" {
        respondError(w, http.StatusBadRequest, "IDが指定されていません")
        return
    }

    log.Printf("🔍 GET /items/%s", id)

    item, err := getItemFromDynamo(id)
    if err != nil {
        respondError(w, http.StatusInternalServerError, "DynamoDBからの取得に失敗しました")
        return
    }

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

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

// DynamoDBに登録
func putItemToDynamo(item Item) error {
    av, err := attributevalue.MarshalMap(item)
    if err != nil {
        return err
    }

    _, err = svc.PutItem(context.TODO(), &dynamodb.PutItemInput{
        TableName: aws.String("Go-Practice-Users"),
        Item:      av,
    })

    return err
}

// DynamoDBから取得
func getItemFromDynamo(id string) (*Item, error) {
    input := &dynamodb.GetItemInput{
        TableName: aws.String("Go-Practice-Users"),
        Key: map[string]types.AttributeValue{
            "UserID":  &types.AttributeValueMemberS{Value: id},
            "sortKey": &types.AttributeValueMemberS{Value: "User"},
        },
    }

    result, err := svc.GetItem(context.TODO(), input)
    if err != nil {
        return nil, err
    }

    if result.Item == nil {
        return nil, nil
    }

    var item Item
    if err := attributevalue.UnmarshalMap(result.Item, &item); err != nil {
        return nil, err
    }

    return &item, nil
}

// 共通エラーレスポンス
func respondError(w http.ResponseWriter, code int, msg string) {
    log.Printf("⚠ エラー [%d]: %s", code, msg)
    w.Header().Set("Content-Type", "application/json")
    w.WriteHeader(code)
    json.NewEncoder(w).Encode(map[string]string{"error": msg})
}

// クライアント初期化
func initDynamoClient() *dynamodb.Client {
    cfg, err := config.LoadDefaultConfig(context.TODO())
    if err != nil {
        log.Fatalf("AWS設定の読み込みに失敗: %v", err)
    }
    return dynamodb.NewFromConfig(cfg)
}

今日の学びポイント

28日目の要点
  • 処理の構造を明確に分けると、コードの読みやすさが劇的に向上!
  • エラーハンドリングが一元化されると、保守性・拡張性が高まる
  • ログを入れることでAPIの流れが「見える化」されて安心感が増した

今回、「設計っぽさ」が出てきて、ちょっと開発者らしくなった気がしますね。 今までの書き捨てっぽいコードから一歩進んだ感触で、HTTPステータスコードやログも意識できるようになっていかなきゃな。。。 エラー処理って地味だけど、やっぱり大事ですね。

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