チュートリアル: Go と Gin で RESTful API を開発する
このチュートリアルでは、Go と Gin Web Framework (Gin) を使用して RESTful ウェブサービス API を記述する基本的な方法を紹介します。
Go とそのツールに基本的な知識があれば、このチュートリアルを最大限に活用できます。Go を初めて使用する場合は、簡単な紹介としてチュートリアル: Go を始めよう を参照してください。
Gin は、ウェブサービスを含むウェブアプリケーションの構築に関連する多くのコーディングタスクを簡素化します。このチュートリアルでは、Gin を使用してリクエストをルーティングし、リクエストの詳細を取得し、レスポンスの JSON をマーシャリングします。
このチュートリアルでは、2つのエンドポイントを持つ RESTful API サーバーを構築します。例のプロジェクトは、ビンテージジャズレコードに関するデータのレポジトリになります。
このチュートリアルには以下のセクションが含まれています。
- API エンドポイントを設計します。
- コード用のフォルダーを作成します。
- データを作成します。
- すべてのアイテムを返すハンドラを記述します。
- 新しいアイテムを追加するハンドラを記述します。
- 特定のアイテムを返すハンドラを記述します。
注:他のチュートリアルについては、チュートリアルをご覧ください。
このチュートリアルを Google 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 として返します。
次に、コード用のフォルダーを作成します。
コード用のフォルダーを作成する
まず、記述するコードのプロジェクトを作成します。
-
コマンドプロンプトを開き、ホームディレクトリに移動します。
LinuxまたはMacの場合
$ cdWindowsの場合
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 ファイルを作成します。モジュールパスでモジュールに名前を付けることの詳細については、依存関係の管理 を参照してください。
次に、データを処理するためのデータ構造を設計します。
データを作成する
チュートリアルのために物事をシンプルにするため、データはメモリに保存します。より一般的な API はデータベースと対話します。
データをメモリに保存すると、サーバーを停止するたびにアルバムのセットが失われ、起動時に再作成されることに注意してください。
コードを書く
-
テキストエディタを使用して、web-service ディレクトリに main.go というファイルを作成します。このファイルに Go コードを記述します。
-
main.go のファイルの先頭に、以下のパッケージ宣言を貼り付けます。
package mainスタンドアロンプログラム (ライブラリとは対照的に) は常にパッケージ
mainにあります。 -
パッケージ宣言の下に、
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"` } -
追加した構造体宣言の下に、開始に使用するデータを含む
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 として返したいとします。
これを行うには、以下を記述します。
- レスポンスを準備するロジック
- リクエストパスをロジックにマッピングするコード
これらが実行時には逆順で実行されることに注意してください。しかし、あなたはまず依存関係を追加し、次にそれらに依存するコードを追加します。
コードを書く
-
前のセクションで追加した構造体コードの下に、アルバムリストを取得するための次のコードを貼り付けます。
この
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.IndentedJSONをContext.JSONの呼び出しに置き換えることができることに注意してください。実際には、インデントされた形式の方がデバッグ時に扱いやすく、サイズの違いは通常小さいです。
-
-
main.go の先頭近く、
albumsスライス宣言のすぐ下に、以下のコードを貼り付けて、ハンドラ関数をエンドポイントパスに割り当てます。これは、
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.2Go は、前のステップで追加した
import宣言を満たすために、この依存関係を解決してダウンロードしました。 -
main.go を含むディレクトリでコマンドラインからコードを実行します。「現在のディレクトリのコードを実行する」という意味でドット引数を使用します。
$ go run .コードが実行されると、リクエストを送信できる実行中の HTTP サーバーがあります。
-
新しいコマンドラインウィンドウから、
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 リクエストを処理するコードで別のエンドポイントを作成します。
新しいアイテムを追加するハンドラを記述する
クライアントが /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 から初期化された
album構造体をalbumsスライスに追加します。 - 追加したアルバムを表す JSON とともに、
201ステータスコードをレスポンスに追加します。
-
以下の例のように、
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 メソッドとパスの組み合わせに関連付けることができます。このようにして、クライアントが使用しているメソッドに基づいて、単一のパスに送信されたリクエストを個別にルーティングできます。
-
コードを実行する
-
サーバーが前のセクションからまだ実行されている場合は、停止してください。
-
main.goを含むディレクトリのコマンドラインから、コードを実行します。
$ go run . -
別のコマンドラインウィンドウから、
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 } -
前のセクションと同様に、
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 パスパラメータと一致するアルバムを返したいとします。
これを行うには、以下を行います。
- 要求されたアルバムを取得するロジックを追加します。
- パスをロジックにマッピングします。
コードを書く
-
前のセクションで追加した
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 OKHTTP コードとともにレスポンスとして返します。前述のとおり、実際のサービスではこの検索にデータベースクエリを使用する可能性が高いでしょう。
-
アルバムが見つからなかった場合は、
http.StatusNotFoundで HTTP404エラーを返します。
-
-
最後に、以下の例に示すように、パスが
/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 では、パスのアイテムの前に付くコロンは、そのアイテムがパスパラメータであることを意味します。
コードを実行する
-
サーバーが前のセクションからまだ実行されている場合は、停止してください。
-
main.go を含むディレクトリでコマンドラインからコードを実行してサーバーを起動します。
$ go run . -
別のコマンドラインウィンドウから、
curlを使用して実行中のウェブサービスにリクエストを送信します。$ curl https://:8080/albums/2コマンドは、使用した ID のアルバムの JSON を表示するはずです。アルバムが見つからなかった場合は、エラーメッセージを含む JSON が表示されます。
{ "id": "2", "title": "Jeru", "artist": "Gerry Mulligan", "price": 17.99 }
まとめ
おめでとうございます!Go と Gin を使用して、シンプルな RESTful ウェブサービスを記述しました。
次に推奨されるトピック
- Go を初めて使用する場合は、Effective Go と Go コードの記述方法 で役立つベストプラクティスが説明されています。
- Goツアーは、Goの基本を段階的に紹介する優れた入門書です。
- Gin の詳細については、Gin Web Framework パッケージドキュメント または Gin Web Framework docs を参照してください。
完成したコード
このセクションには、このチュートリアルで構築するアプリケーションのコードが含まれています。
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"})
}
