チュートリアル:GoとGinを使ったRESTful APIの開発

このチュートリアルでは、GoとGin Web Framework(Gin)を使用して、RESTful WebサービスAPIを作成する基礎を紹介します。

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

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

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

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

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

注:その他のチュートリアルについては、チュートリアルを参照してください。

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

Open in Cloud Shell

前提条件

APIエンドポイントを設計する

ビニール盤でのヴィンテージ録音を販売するストアへのアクセスを提供するAPIを構築します。したがって、クライアントがユーザーのアルバムを取得および追加できるエンドポイントを提供する必要があります。

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

このチュートリアルで作成するエンドポイントは次のとおりです。

/albums

/albums/:id

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

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

まず、記述するコードのプロジェクトを作成します。

  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ステータスコードです。ここでは、net/httpパッケージのStatusOK定数を渡して、200 OKを示しています。

      Context.IndentedJSONContext.JSONの呼び出しに置き換えて、よりコンパクトな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を使用して実行中のWebサービスにリクエストを行います。

    $ 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リクエストを処理してアイテムを追加するコードを使用して、別のエンドポイントを作成します。

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

クライアントが/albumsPOSTリクエストを行う場合、リクエスト本文に記述されたアルバムを既存のアルバムのデータに追加したいと考えます。

これを行うには、次のことを記述します

コードを記述する

  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. 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を使用して実行中のWebサービスにリクエストを行います。

    $ 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]にリクエストを送信すると、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を使用して実行中のWebサービスにリクエストを行います。

    $ curl http://localhost:8080/albums/2
    

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

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

結論

おめでとうございます!GoとGinを使用して、シンプルなRESTful Webサービスを作成しました。

次の推奨トピック

完成したコード

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

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