Goの公式チュートリアルをやってみる【RESTful API】

前回

Goの公式チュートリアルをやってみる【リレーショナルデータベース】

はじめに

前回はリレーショナルデータベースに接続して操作する方法を学びました。今回はGin Web Framework (Gin)を使用してRESTfulなWebサービスAPIを書く方法を学んでいきます。

Ginはリクエストをルーティングし、リクエストの詳細を取得、レスポンスのJSONをマーシャルするために使用します。

今回は2つのエンドポイントをもつRESTfulなAPIサーバーを構築します。リポジトリはビンテージジャズのレコードのデータになります。

APIエンドポイントの設計

ビンテージのレコードを販売する店へのアクセスを提供するAPIを設計していきます。APIを開発する場合はエンドポイントから設計し始めるのが基本です。このチュートリアルで作成するエンドポイントは以下の通りです。

/albums

  • GET:アルバムのリストを取得し、JSONとして返す。
  • POST:JSONとして送信されたリクエストデータから新しいアルバムを追加する。

/albums/:id

  • GET – IDでアルバムを取得し、JSONとしてアルバムデータを返します。

モジュールを作成

まずはweb-service-ginフォルダを作成します。その後コマンドプロンプトでweb-service-ginディレクトリに移動しましょう。以下コマンドを入力して同じ内容が出力されたら作成完了です。

$ go mod init example/web-service-gin
go: creating new go.mod: module example/web-service-gin

データの作成

web-service-ginディレクトリに移動しmain.goファイルを作成します。以下のコードを追加します。

main.go
package main

// レコードアルバムを表すデータ
type album struct {
    ID     string  `json:"id"`
    Title  string  `json:"title"`
    Artist string  `json:"artist"`
    Price  float64 `json:"price"`
}

// スライスでデータを用意する。
var albums = []album{
    {ID: "1", Title: "Blue Train", Artist: "John Coltrane", Price: 56.99},
    {ID: "2", Title: "Jeru", Artist: "Gerry Mulligan", Price: 17.99},
    {ID: "3", Title: "Sarah Vaughan and Clifford Brown", Artist: "Sarah Vaughan", Price: 39.99},
}

構造体の各フィールドの末尾に``で囲まれた文字列があります。これは構造体タグと呼ばれるメタ情報で、`key:"value"`という形式で表されることが多いです。

json:"artist"などの構造体タグは、構造体のコンテンツがJSONにシリアライズされるときのフィールド名を表します。このタグがないと、JSONでは構造体の大文字のフィールド名がデフォルトで使用されます。

すべての項目を返すハンドラーの追加

先ほどGET /albumsでリクエストを行うとすべてのアルバムをJSONで返すと決めました。リクエストを受け取った際に実行する処理を用意する必要があります。main.goの下に以下のコードを追加しましょう。

main.go
// すべてのアルバムのリストをJSONで返す
func getAlbums(c *gin.Context) {
    c.IndentedJSON(http.StatusOK, albums)
}

このコードはこんなことをしています。

  • ginが要求するgin.Contextパラメータを受け取る関数を用意する。ginが要求するのはパラメータのみで関数名は自由です。
    gin.ContextはGinの重要な要素で、リクエストの詳細・JSONの検証・シリアライズなどを行います。
  • Context.IndentedJSONを呼び出してalubumsの構造体スライスをJSONにシリアライズしてレスポンスに追加します。第一引数はHTTPステータスコードを表していて、今回は200(OK)を表すStatusOKを渡しています。

    Context.IndentedJSONContext.JSONに置き換えると、さらにコンパクトなJSONになります。しかしインデントされた形式のほうがデバッグ時の作業が楽なので、今回はContext.IndentedJSONを使用しています。

続いて、albumsスライスの宣言の真下に以下のmain関数を追加しましょう。

main.go
func main() {
    router := gin.Default()
    router.GET("/albums", getAlbums)

    router.Run("localhost:8080")
}

このmain関数はこんなことをしています。

  • Ginルーターをデフォルト設定で初期化
  • GET関数を使用してGET HTTPメソッドと/albumsパスをハンドラー関数に関連付ける。
    ここでは関数名を入力して関数を渡すことに注意しましょう(()を付けると「関数を渡している」のではなく、「関数を実行した結果」を渡すことになります。)
  • Run関数でルーターをhttp.Serverに接続する

最後に使用するパッケージをインポートしてコードの完成です。package宣言の真下に以下の内容を追加しましょう。

main.go
import (
    "net/http"

    "github.com/gin-gonic/gin"
)

コードが完成したので実行確認をしてみましょう。まずはコマンドプロンプトでweb-server-ginディレクトリに移動します。新しい外部モジュールを追加したのでgo mod tidyを実行して依存関係を整理しましょう。依存関係の整理が終えたらgo run .を実行してHTTPサーバーを起動します。

その後、新しいコマンドプロンプトを開き以下のコマンドを実行します。以下のような出力結果が返されたら、GETリクエストに対する応答の成功です。

[
    {
        "id": "1",
        "title": "Blue Train",
        "artist": "John Coltrane",
        "price": 56.99
    },
    {
        "id": "2",
        "title": "Jeru",
        "artist": "Gerry Mulligan",
        "price": 17.99
    },
    {
        "id": "3",
        "title": "Sarah Vaughan and Clifford Brown",
        "artist": "Sarah Vaughan",
        "price": 39.99
    }
]

データを追加するハンドラーの追加

続いて、アルバムのデータをリストに追加するコードを追加していきましょう。main.goの最後に以下の関数を追加します。

main.go
// 受け取ったJSONからアルバムを追加する
func postAlbums(c *gin.Context) {
    var newAlbum album

    // 受け取ったJSONをnewAlbumにバインドする
    if err := c.BindJSON(&newAlbum); err != nil {
        return
    }

    // 新しいアルバムをスライスに追加する
    albums = append(albums, newAlbum)
    c.IndentedJSON(http.StatusCreated, newAlbum)
}

このコードではこんなことをしています。

  • Context.BindJSONを使用して、リクエスト・ボディをnewAlbumにバインドする
  • JSONから初期化されたアルバム構造体をスライスに追加する
  • 追加したアルバムを表すJSONと201ステータスコードをレスポンスに追加する

続いてPOSTリクエストで先ほどの関数を実行するようにmain関数に処理を追加しましょう。以下の様にmain関数を変更してください。

main.go
func main() {
    router := gin.Default()
    router.GET("/albums", getAlbums)
    router.POST("/albums", postAlbums)

    router.Run("localhost:8080")
}

先ほどGETの確認でサーバーを起動していました。まだ起動している場合は一度閉じて再度go run .を実行してサーバーを再起動しましょう。

そしてもう片方のコマンドプロンプトで以下のコマンドを実行してください。(コマンドプロンプトやPowerShell等で書き方が変わるのでご注意ください)

$ curl http://localhost:8080/albums ^
    --include ^
    --header "Content-Type: application/json" ^
    --request POST ^
    --data "{\"id\": \"4\",\"title\": \"The Modern Sound of Betty Carter\",\"artist\": \"Betty Carter\",\"price\": 49.99}"

以下のような出力結果が得られれば、POSTの成功です。

HTTP/1.1 201 Created
Content-Type: application/json; charset=utf-8
Date: Sun, 18 Aug 2024 13:09:10 GMT
Content-Length: 116

{
    "id": "4",
    "title": "The Modern Sound of Betty Carter",
    "artist": "Betty Carter",
    "price": 49.99
}

追加されたかを再度GETして確認してみましょう。以下のコマンドを実行してアルバムを取得します。

$ curl http://localhost:8080/albums ^
    --header "Content-Type: application/json" ^
    --request "GET"

以下の様に4つのデータが出力されたらリストへの追加が成功しています。

[
        {
                "id": "1",
                "title": "Blue Train",
                "artist": "John Coltrane",
                "price": 56.99
        },
        {
                "id": "2",
                "title": "Jeru",
                "artist": "Gerry Mulligan",
                "price": 17.99
        },
        {
                "id": "3",
                "title": "Sarah Vaughan and Clifford Brown",
                "artist": "Sarah Vaughan",
                "price": 39.99
        },
        {
                "id": "4",
                "title": "The Modern Sound of Betty Carter",
                "artist": "Betty Carter",
                "price": 49.99
        }
]

特定のデータを返すハンドラーの追加

ここまでで『すべてのデータの取得』、『一つのデータを追加』を実装しました。次は指定されたIDから一つのデータのみを取得するコードを追加していきます。main.goの最後に以下の関数を追加しましょう。

main.go
// 受信したidパラメーターとIDフィールドの値が一致するアルバムを探し、そのアルバムを応答として返す
func getAlbumByID(c *gin.Context) {
    id := c.Param("id")

    // アルバムのリストをループしてIDフィールドの値がパラメーターと一致するアルバムを探す
    for _, a := range albums {
        if a.ID == id {
            c.IndentedJSON(http.StatusOK, a)
            return
        }
    }
    c.IndentedJSON(http.StatusNotFound, gin.H{"message": "album not found"})
}

ここではこんなことをしています。

  • Context.Param関数を使用してURLからidパスパラメーターを取得する。
    このハンドラをパスにマップするとパスにパラメーターのプレースホルダーが含まれます。
  • スライス内のalbum構造体をループしてIDフィールドの値が一致するものを探す。見つかった場合はそのalbum構造体をJSONにシリアライズして、HTTPコード200 OKの応答として返す。
  • 指定されたIDのアルバムが見つからない場合はhttp.StatusNotFoundでHTTP404エラーを返す。

追加した関数をGETリクエストで実行するようにmain関数を以下の様に変更しましょう。この際、パスは/albums/:idとなります。

main.go
func main() {
    router := gin.Default()
    router.GET("/albums", getAlbums)
    router.GET("/albums/:id", getAlbumByID)
    router.POST("/albums", postAlbums)

    router.Run("localhost:8080")
}

コードを変更したらgo run .でサーバーを再起動しましょう。その後、以下のコマンドを実行して出力を確認してみましょう。2番のアルバムのみが返ってきたら成功です。

$ curl http://localhost:8080/albums/2
{
        "id": "2",
        "title": "Jeru",
        "artist": "Gerry Mulligan",
        "price": 17.99
}

まとめ

  • ginを使用すると簡単にRESTfulなAPIを作成することができる。
  • gin.Default等でルーターを取得できる。
  • ルーターのGet関数やPost関数を使用するとリクエストに対するハンドラーを設定できる。
  • ルーターに渡すハンドラーには引数に*gin.Context型を指定しなければならない

次回

Goの公式チュートリアルをやってみる【ファジング】

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です