🙌

GoとGinでRESTful APIを開発する

2024/05/28に公開

https://go.dev/doc/tutorial/web-service-gin

Tutorial: Developing a RESTful API with Go and Gin

このチュートリアルでは、GoとGin Web Framework(Gin)を使ってRESTfulなWebサービスAPIを書く基本を紹介します。

Ginは、Webサービスを含むWebアプリケーションの構築に関連する多くのコーディング作業を簡素化します。このチュートリアルでは、Ginを使ってリクエストをルーティングし、リクエストの詳細を取得し、レスポンス用のJSONをマーシャルします。

このチュートリアルでは、2つのエンドポイントを持つRESTful APIサーバーを構築します。サンプルプロジェクトは、ビンテージジャズのレコードに関するデータのリポジトリです。

このチュートリアルには以下のセクションがあります:

  1. API エンドポイントを設計する
  2. コード用のフォルダを作成する
  3. データを作成する
  4. すべての項目を返すハンドラを記述する
  5. 新しいアイテムを追加するハンドラを書く
  6. 特定のアイテムを返すハンドラを書く

前提条件

  • Go 1.16以降のインストール インストール手順については、Goのインストールを参照してください
  • コードを編集するツール テキストエディタであれば何でも構いません
  • コマンド・ターミナル Goは、LinuxやMacではどのターミナルでも、WindowsではPowerShellやcmdでもうまく動作します
  • curlツール LinuxとMacでは、すでにインストールされているはずだ。Windowsでは、Windows 10 Insiderビルド17063以降に含まれている。それ以前のバージョンのWindowsでは、インストールする必要があるかもしれない。詳しくは、Tar and Curl Come to Windowsを参照のこと

APIエンドポイントの設計

ビンテージ盤を販売する店舗へのアクセスを提供するAPIを構築します。そのため、クライアントがユーザーのためにアルバムを取得したり追加したりできるエンドポイントを提供する必要がある。

APIを開発する場合、通常はエンドポイントを設計することから始める。エンドポイントが理解しやすいものであれば、APIのユーザーはより成功を収めることができるだろう。

以下は、このチュートリアルで作成するエンドポイントです。

/albums

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

/albums/:id

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

次に、コード用のフォルダを作成します。

コード用のフォルダを作成する

まず、これから書くコードのプロジェクトを作成する。

  1. コマンドプロンプトを開き、ホームディレクトリに移動する
    LinuxまたはMac:

        $ cd
    

    Windows:

        C:\> cd %HOMEPATH%
    
  2. コマンド・プロンプトを使って、web-service-ginというコード用のディレクトリを作成する

        $ mkdir web-service-gin
        $ cd web-service-gin
    
  3. 依存関係を管理できるモジュールを作成する。
    go mod initコマンドを実行し、コードを入れるモジュールのパスを指定する

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

    このコマンドはgo.modファイルを作成し、そこにあなたが追加した依存関係がトラッキングのためにリストアップされます。モジュールパスを使ったモジュール名の付け方については、Managing dependenciesを参照してください。

次に、データを扱うためのデータ構造を設計する。

データを作成する

チュートリアルでは物事をシンプルにするために、データをメモリーに保存する。もっと典型的なAPIはデータベースとやりとりする。

データをメモリーに保存するということは、サーバーを停止するたびにアルバムのセットが失われ、サーバーを起動すると再作成されるということです。

コードを書く

  1. テキストエディタを使って、web-serviceディレクトリにmain.goというファイルを作成します。このファイルにGoコードを記述します

  2. main.goのファイルの一番上に、以下のパッケージ宣言を貼り付ける

        package main
    

    スタンドアロン・プログラムは(ライブラリとは対照的に)常にmainパッケージに入っている。

  3. パッケージ宣言の下に、以下のalbum構造体の宣言を貼り付ける。これを使ってアルバム・データをメモリーに保存する
    json: "artist"などの構造体タグは、構造体のコンテンツがJSONにシリアライズされるときのフィールド名を指定します。このタグがないと、JSONでは構造体の大文字のフィールド名が使用されます

        // album represents data about a record album.
        type album struct {
            ID     string  `json:"id"`
            Title  string  `json:"title"`
            Artist string  `json:"artist"`
            Price  float64 `json:"price"`
        }
    
  4. 先ほど追加した構造体宣言の下に、最初に使用するデータを含むアルバム構造体の以下のスライスを貼り付けます

        // albums slice to seed record album data.
        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},
        }
    

次に、最初のエンドポイントを実装するコードを書きます。

すべての項目を返すハンドラを書く

クライアントがGET /albumsでリクエストを行うと、すべてのアルバムをJSONとして返したい。

そのためには、次のように書く:

  • レスポンスを準備するロジック
  • リクエストパスをロジックにマップするコード

これは実行時に実行される方法の逆であることに注意してください。

コードを書く

  1. 前のセクションで追加した構造体コードの下に、以下のコードを貼り付けてアルバム・リストを取得します。
    このgetAlbums関数は、アルバム構造体のスライスからJSONを作成し、そのJSONをレスポンスに書き込みます

        // getAlbums responds with the list of all albums as JSON.
        func getAlbums(c *gin.Context) {
            c.IndentedJSON(http.StatusOK, albums)
        }
    

    このコードでは、あなたは

    • gin.Contextパラメータを受け取るgetAlbums関数を書いてください。GinもGoも特定の関数名形式を要求しているわけではありません
      gin.ContextはGinの最も重要な部分です。リクエストの詳細、JSONの検証、シリアライズなどを行います。(名前は似ていますが、Goの組み込みcontextパッケージとは異なります)。
    • Context.IndentedJSONを呼び出して構造体をJSONにシリアライズし、レスポンスに追加します
      この関数の最初の引数は、クライアントに送りたいHTTPステータスコードです。ここでは、200 OKを示すためにnet/httpパッケージのStatusOK定数を渡しています。
      Context.IndentedJSONContext.JSONの呼び出しに置き換えることで、よりコンパクトなJSONを送信できることに注意してください。実際には、インデントされた形式の方がデバッグ時の作業がはるかに簡単で、サイズの差も通常は小さくなります。
  2. main.goの一番上、アルバム・スライス宣言のすぐ下に、以下のコードを貼り付けて、ハンドラー関数をエンドポイント・パスに割り当てる
    これにより、getAlbums/albumsエンドポイントパスへのリクエストを処理する関連付けが設定されます

        func main() {
            router := gin.Default()
            router.GET("/albums", getAlbums)
        
            router.Run("localhost:8080")
        }
    

    このコードでは、あなたは

    • Defaultを使ってGinルーターを初期化する
    • GET関数を使って、GET HTTPメソッドと/albumsパスをハンドラ関数に関連付ける
      getAlbums関数の名前を渡していることに注意してください。これは、getAlbums()を渡して行う関数の結果を渡すのとは異なります(括弧に注意)
    • ルータをhttp.Serverに接続し、サーバを起動するには、Run関数を使用します
  3. main.goの一番上、パッケージ宣言のすぐ下に、今書いたコードをサポートするために必要なパッケージをインポートする。
    コードの最初の行は次のようにする:

        package main
    
        import (
            "net/http"
        
            "github.com/gin-gonic/gin"
        )
    
  4. main.goを保存する

コードを実行する

  1. 依存関係としてGinモジュールのトラッキングを開始する
    コマンドラインで、go getを使ってgithub.com/gin-gonic/ginモジュールをあなたのモジュールの依存関係として追加します。ドット引数を使うと、"カレントディレクトリのコードの依存関係を取得する "という意味になります

        $ go get .
        go get: added github.com/gin-gonic/gin v1.7.2
    

    Goは、前のステップで追加したインポート宣言を満たすために、この依存関係を解決してダウンロードした。

  2. main.goのあるディレクトリのコマンドラインから、コードを実行する。ドット引数は、"カレント・ディレクトリでコードを実行する "という意味で使う

        $ go run .
    

    コードが実行されれば、リクエストを送ることができるHTTPサーバーが完成する。

  3. 新しいコマンドラインウィンドウから、curlを使って実行中のウェブサービスにリクエストを出す。

        $ curl http://localhost:8080/albums
    

    コマンドは、あなたがサービスに定義したデータを表示するはずです。

        [
                {
                        "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
                }
        ]
    

    APIが開始されました!次のセクションでは、アイテムを追加するためのPOSTリクエストを処理するコードで別のエンドポイントを作成します。

新しい項目を追加するハンドラを書く

クライアントが/albumsにPOSTリクエストをしたとき、リクエストボディに記述されたアルバムを既存のアルバムデータに追加したい。

そのためには、次のように書く:

  • 新しいアルバムを既存のリストに追加するロジック
  • POSTリクエストをあなたのロジックにルーティングするちょっとしたコード

コードを書く

  1. アルバムリストにアルバムデータを追加するコードを追加する
    import文の後のどこかに、以下のコードを貼り付ける。(このコードはファイルの最後が良いのですが、Goは関数を宣言する順番を強制しません)

        // postAlbums adds an album from JSON received in the request body.
        func postAlbums(c *gin.Context) {
            var newAlbum album
        
            // Call BindJSON to bind the received JSON to
            // newAlbum.
            if err := c.BindJSON(&newAlbum); err != nil {
                return
            }
        
            // Add the new album to the slice.
            albums = append(albums, newAlbum)
            c.IndentedJSON(http.StatusCreated, newAlbum)
        }
    

    このコードでは、あなたは

    • Context.BindJSONを使用して、リクエスト・ボディをnewAlbumにバインドします
    • JSONから初期化されたアルバム構造体をalbumsスライスに追加します
    • 追加したアルバムを表すJSONとともに、レスポンスに201ステータスコードを追加します
  2. 次のように、main関数にrouter.POST関数を含めるように変更します

        func main() {
            router := gin.Default()
            router.GET("/albums", getAlbums)
            router.POST("/albums", postAlbums)
        
            router.Run("localhost:8080")
        }
    

    このコードでは、あなたは

    • /albumsパスのPOSTメソッドとpostAlbums関数を関連付ける
    • Ginでは、HTTPメソッドとパスの組み合わせにハンドラを関連付けることができます。このように、クライアントが使用しているメソッドに基づいて、単一のパスに送信されたリクエストを個別にルーティングすることができます

コードを実行する

  1. 最後のセクションでサーバーがまだ動いている場合は、サーバーを停止する

  2. main.goのあるディレクトリのコマンドラインから、コードを実行する

        $ go run .
    
  3. 別のコマンドラインウィンドウから、curlを使って実行中のウェブサービスにリクエストを出す

        $ 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}'
    

    このコマンドは、追加されたアルバムのヘッダーとJSONを表示する。

        HTTP/1.1 201 Created
        Content-Type: application/json; charset=utf-8
        Date: Wed, 02 Jun 2021 00:34:12 GMT
        Content-Length: 116
        
        {
            "id": "4",
            "title": "The Modern Sound of Betty Carter",
            "artist": "Betty Carter",
            "price": 49.99
        }
    
  4. 前のセクションと同様に、curlを使ってアルバムの全リストを取得し、それを使って新しいアルバムが追加されたことを確認する

        $ curl http://localhost:8080/albums \
            --header "Content-Type: application/json" \
            --request "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
                },
                {
                        "id": "4",
                        "title": "The Modern Sound of Betty Carter",
                        "artist": "Betty Carter",
                        "price": 49.99
                }
        ]
    

    次のセクションでは、特定のアイテムのGETを処理するコードを追加します。

特定の項目を返すハンドラを書く

クライアントがGET /albums/[id]というリクエストをしたとき、IDidパス・パラメータと一致するアルバムを返したい。

そのためには:

  • リクエストされたアルバムを取得するロジックを追加する
  • パスをロジックにマップする

コードを書く

  1. 前のセクションで追加したpostAlbums関数の下に、特定のアルバムを取得するために以下のコードを貼り付けます
    このgetAlbumByID関数は、リクエストパスのIDを抽出し、一致するアルバムを探します

        // getAlbumByID locates the album whose ID value matches the id
        // parameter sent by the client, then returns that album as a response.
        func getAlbumByID(c *gin.Context) {
            id := c.Param("id")
        
            // Loop over the list of albums, looking for
            // an album whose ID value matches the parameter.
            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パスパラメータを取得します。このハンドラをパスにマップするとき、パスにパラメータのプレースホルダを含めます

    • スライス内のアルバム構造体をループして、IDフィールドの値がidパラメータの値と一致するものを探します。見つかった場合は、そのアルバム構造体をJSONにシリアライズし、HTTPコード200 OKで応答として返します

    • 前述のとおり、実際のサービスでは、データベースクエリを使用してこの検索を実行する可能性があります

    • アルバムが見つからない場合は、http.StatusNotFoundHTTP 404エラーを返します

  2. 最後に、次の例に示すように、mainを変更して、router.GETの新しい呼び出しを含むようにします。ここで、パスは/albums/:idになります

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

    このコードでは、あなたは:

    • albums/:idパスをgetAlbumByID関数に関連付けます。Ginでは、パスの項目の前にあるコロンは、その項目がパスのパラメータであることを意味します

コードを実行する

  1. 最後のセクションでサーバーがまだ動いている場合は、サーバーを停止する
  2. main.goのあるディレクトリのコマンドラインから、サーバーを起動するコードを実行する
        $ go run .
    
  3. 別のコマンドラインウィンドウから、curlを使って実行中のウェブサービスにリクエストを出す
        $ curl http://localhost:8080/albums/2
    
    コマンドは、使用したIDのアルバムのJSONを表示するはずです。アルバムが見つからなかった場合は、エラーメッセージとともにJSONが表示されます。
        {
                "id": "2",
                "title": "Jeru",
                "artist": "Gerry Mulligan",
                "price": 17.99
        }
    

結論

おめでとうございます!GoとGinを使ってシンプルなRESTfulウェブサービスを書きましたね。

完成したコード

このセクションには、このチュートリアルで作成するアプリケーションのコードが含まれています。

main.go
    package main

    import (
        "net/http"
    
        "github.com/gin-gonic/gin"
    )
    
    // album represents data about a record album.
    type album struct {
        ID     string  `json:"id"`
        Title  string  `json:"title"`
        Artist string  `json:"artist"`
        Price  float64 `json:"price"`
    }
    
    // albums slice to seed record album data.
    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},
    }
    
    func main() {
        router := gin.Default()
        router.GET("/albums", getAlbums)
        router.GET("/albums/:id", getAlbumByID)
        router.POST("/albums", postAlbums)
    
        router.Run("localhost:8080")
    }
    
    // getAlbums responds with the list of all albums as JSON.
    func getAlbums(c *gin.Context) {
        c.IndentedJSON(http.StatusOK, albums)
    }
    
    // postAlbums adds an album from JSON received in the request body.
    func postAlbums(c *gin.Context) {
        var newAlbum album
    
        // Call BindJSON to bind the received JSON to
        // newAlbum.
        if err := c.BindJSON(&newAlbum); err != nil {
            return
        }
    
        // Add the new album to the slice.
        albums = append(albums, newAlbum)
        c.IndentedJSON(http.StatusCreated, newAlbum)
    }
    
    // getAlbumByID locates the album whose ID value matches the id
    // parameter sent by the client, then returns that album as a response.
    func getAlbumByID(c *gin.Context) {
        id := c.Param("id")
    
        // Loop through the list of albums, looking for
        // an album whose ID value matches the parameter.
        for _, a := range albums {
            if a.ID == id {
                c.IndentedJSON(http.StatusOK, a)
                return
            }
        }
        c.IndentedJSON(http.StatusNotFound, gin.H{"message": "album not found"})
    }

Discussion