GoとGinでRESTful APIを開発する
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サーバーを構築します。サンプルプロジェクトは、ビンテージジャズのレコードに関するデータのリポジトリです。
このチュートリアルには以下のセクションがあります:
- API エンドポイントを設計する
- コード用のフォルダを作成する
- データを作成する
- すべての項目を返すハンドラを記述する
- 新しいアイテムを追加するハンドラを書く
- 特定のアイテムを返すハンドラを書く
前提条件
- 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として返す
次に、コード用のフォルダを作成します。
コード用のフォルダを作成する
まず、これから書くコードのプロジェクトを作成する。
-
コマンドプロンプトを開き、ホームディレクトリに移動する
LinuxまたはMac:$ cd
Windows:
C:\> cd %HOMEPATH%
-
コマンド・プロンプトを使って、
web-service-gin
というコード用のディレクトリを作成する$ mkdir web-service-gin $ cd web-service-gin
-
依存関係を管理できるモジュールを作成する。
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はデータベースとやりとりする。
データをメモリーに保存するということは、サーバーを停止するたびにアルバムのセットが失われ、サーバーを起動すると再作成されるということです。
コードを書く
-
テキストエディタを使って、
web-service
ディレクトリにmain.go
というファイルを作成します。このファイルにGoコードを記述します -
main.go
のファイルの一番上に、以下のパッケージ宣言を貼り付けるpackage main
スタンドアロン・プログラムは(ライブラリとは対照的に)常に
main
パッケージに入っている。 -
パッケージ宣言の下に、以下の
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"` }
-
先ほど追加した構造体宣言の下に、最初に使用するデータを含むアルバム構造体の以下のスライスを貼り付けます
// 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として返したい。
そのためには、次のように書く:
- レスポンスを準備するロジック
- リクエストパスをロジックにマップするコード
これは実行時に実行される方法の逆であることに注意してください。
コードを書く
-
前のセクションで追加した構造体コードの下に、以下のコードを貼り付けてアルバム・リストを取得します。
この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.IndentedJSON
をContext.JSON
の呼び出しに置き換えることで、よりコンパクトなJSONを送信できることに注意してください。実際には、インデントされた形式の方がデバッグ時の作業がはるかに簡単で、サイズの差も通常は小さくなります。
-
-
main.go
の一番上、アルバム・スライス宣言のすぐ下に、以下のコードを貼り付けて、ハンドラー関数をエンドポイント・パスに割り当てる
これにより、getAlbums
が/albums
エンドポイントパスへのリクエストを処理する関連付けが設定されますfunc main() { router := gin.Default() router.GET("/albums", getAlbums) router.Run("localhost:8080") }
このコードでは、あなたは
-
main.go
の一番上、パッケージ宣言のすぐ下に、今書いたコードをサポートするために必要なパッケージをインポートする。
コードの最初の行は次のようにする:package main import ( "net/http" "github.com/gin-gonic/gin" )
-
main.go
を保存する
コードを実行する
-
依存関係としてGinモジュールのトラッキングを開始する
コマンドラインで、go get
を使ってgithub.com/gin-gonic/gin
モジュールをあなたのモジュールの依存関係として追加します。ドット引数を使うと、"カレントディレクトリのコードの依存関係を取得する "という意味になります$ go get . go get: added github.com/gin-gonic/gin v1.7.2
Goは、前のステップで追加したインポート宣言を満たすために、この依存関係を解決してダウンロードした。
-
main.go
のあるディレクトリのコマンドラインから、コードを実行する。ドット引数は、"カレント・ディレクトリでコードを実行する "という意味で使う$ go run .
コードが実行されれば、リクエストを送ることができるHTTPサーバーが完成する。
-
新しいコマンドラインウィンドウから、
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リクエストをあなたのロジックにルーティングするちょっとしたコード
コードを書く
-
アルバムリストにアルバムデータを追加するコードを追加する
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
ステータスコードを追加します
-
-
次のように、
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メソッドとパスの組み合わせにハンドラを関連付けることができます。このように、クライアントが使用しているメソッドに基づいて、単一のパスに送信されたリクエストを個別にルーティングすることができます
-
コードを実行する
-
最後のセクションでサーバーがまだ動いている場合は、サーバーを停止する
-
main.go
のあるディレクトリのコマンドラインから、コードを実行する$ go run .
-
別のコマンドラインウィンドウから、
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 }
-
前のセクションと同様に、
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
パス・パラメータと一致するアルバムを返したい。
そのためには:
- リクエストされたアルバムを取得するロジックを追加する
- パスをロジックにマップする
コードを書く
-
前のセクションで追加した
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.StatusNotFound
でHTTP 404
エラーを返します
-
-
最後に、次の例に示すように、
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では、パスの項目の前にあるコロンは、その項目がパスのパラメータであることを意味します
-
コードを実行する
- 最後のセクションでサーバーがまだ動いている場合は、サーバーを停止する
- main.goのあるディレクトリのコマンドラインから、サーバーを起動するコードを実行する
$ go run .
- 別のコマンドラインウィンドウから、
curl
を使って実行中のウェブサービスにリクエストを出すコマンドは、使用したIDのアルバムのJSONを表示するはずです。アルバムが見つからなかった場合は、エラーメッセージとともにJSONが表示されます。$ curl http://localhost:8080/albums/2
{ "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"})
}
Discussion