チュートリアル: Go と Gin で RESTful API を開発する

このチュートリアルでは、Go と Gin Web Framework (Gin) を使用して RESTful ウェブサービス API を記述する基本的な方法を紹介します。

Go とそのツールに基本的な知識があれば、このチュートリアルを最大限に活用できます。Go を初めて使用する場合は、簡単な紹介としてチュートリアル: Go を始めよう を参照してください。

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

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

このチュートリアルには以下のセクションが含まれています。

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

注:他のチュートリアルについては、チュートリアルをご覧ください。

このチュートリアルを Google Cloud Shell でインタラクティブに実行するには、以下のボタンをクリックしてください。

Open in Cloud Shell

前提条件

  • Go 1.16 以降のインストール。 インストール手順については、Go のインストール を参照してください。
  • コードを編集するツール。 任意のテキストエディターで問題ありません。
  • コマンドターミナル. Go は Linux および Mac の任意のターミナル、および Windows の PowerShell または cmd でうまく動作します。
  • curl ツール。 Linux と Mac ではすでにインストールされているはずです。Windows では、Windows 10 Insider ビルド 17063 以降に含まれています。それ以前の Windows バージョンでは、インストールする必要がある場合があります。詳細については、Tar と Curl が 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 ファイルを作成します。モジュールパスでモジュールに名前を付けることの詳細については、依存関係の管理 を参照してください。

次に、データを処理するためのデータ構造を設計します。

データを作成する

チュートリアルのために物事をシンプルにするため、データはメモリに保存します。より一般的な API はデータベースと対話します。

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

コードを書く

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

  2. main.go のファイルの先頭に、以下のパッケージ宣言を貼り付けます。

    package main
    

    スタンドアロンプログラム (ライブラリとは対照的に) は常にパッケージ main にあります。

  3. パッケージ宣言の下に、album 構造体の以下の宣言を貼り付けます。これを使用してアルバムデータをメモリに保存します。

    json:"artist" のような構造体タグは、構造体の内容が JSON にシリアル化されるときにフィールド名が何であるかを指定します。これらがないと、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. 追加した構造体宣言の下に、開始に使用するデータを含む album 構造体の次のスライスを貼り付けます。

    // 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 関数は、album 構造体のスライスから 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 定数を渡しています。

      よりコンパクトな JSON を送信するには、Context.IndentedJSONContext.JSON の呼び出しに置き換えることができることに注意してください。実際には、インデントされた形式の方がデバッグ時に扱いやすく、サイズの違いは通常小さいです。

  2. main.go の先頭近く、albums スライス宣言のすぐ下に、以下のコードを貼り付けて、ハンドラ関数をエンドポイントパスに割り当てます。

    これは、getAlbums/albums エンドポイントパスへのリクエストを処理する関連付けを設定します。

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

    このコードでは、以下のことを行います。

    • Default を使用して Gin ルーターを初期化します。

    • GET 関数を使用して、GET HTTP メソッドと /albums パスをハンドラ関数に関連付けます。

      getAlbums 関数の名前を渡していることに注意してください。これは、関数の結果を渡すこととは異なります。後者は getAlbums() (かっこに注意) を渡すことによって行います。

    • Run 関数を使用して、ルーターを http.Server にアタッチし、サーバーを起動します。

  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 は、前のステップで追加した import 宣言を満たすために、この依存関係を解決してダウンロードしました。

  2. main.go を含むディレクトリでコマンドラインからコードを実行します。「現在のディレクトリのコードを実行する」という意味でドット引数を使用します。

    $ go run .
    

    コードが実行されると、リクエストを送信できる実行中の HTTP サーバーがあります。

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

    $ curl https://: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 リクエストを処理するコードで別のエンドポイントを作成します。

新しいアイテムを追加するハンドラを記述する

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

これを行うには、以下を記述します。

  • 新しいアルバムを既存のリストに追加するロジック。
  • 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 から初期化された album 構造体を albums スライスに追加します。
    • 追加したアルバムを表す JSON とともに、201 ステータスコードをレスポンスに追加します。
  2. 以下の例のように、router.POST 関数を含むように main 関数を変更します。

    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 https://: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 https://: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] にリクエストを行うときに、その ID が id パスパラメータと一致するアルバムを返したいとします。

これを行うには、以下を行います。

  • 要求されたアルバムを取得するロジックを追加します。
  • パスをロジックにマッピングします。

コードを書く

  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 パスパラメータを取得します。このハンドラをパスにマッピングするときに、パスにパラメータのプレースホルダーを含めます。

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

      前述のとおり、実際のサービスではこの検索にデータベースクエリを使用する可能性が高いでしょう。

    • アルバムが見つからなかった場合は、http.StatusNotFound で HTTP 404 エラーを返します。

  2. 最後に、以下の例に示すように、パスが /albums/:id になった router.GET の新しい呼び出しを含むように main を変更します。

    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 https://:8080/albums/2
    

    コマンドは、使用した ID のアルバムの JSON を表示するはずです。アルバムが見つからなかった場合は、エラーメッセージを含む JSON が表示されます。

    {
            "id": "2",
            "title": "Jeru",
            "artist": "Gerry Mulligan",
            "price": 17.99
    }
    

まとめ

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

次に推奨されるトピック

完成したコード

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

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