チュートリアル:GoとGinを使ったRESTful APIの開発
このチュートリアルでは、GoとGin Web Framework(Gin)を使用して、RESTful WebサービスAPIを作成する基礎を紹介します。
Goとそのツールに基本的な知識があれば、このチュートリアルを最大限に活用できます。Goを初めて使用する場合は、簡単な紹介として、チュートリアル:Goを始めようを参照してください。
Ginは、Webサービスを含むWebアプリケーションの構築に関連する多くのコーディングタスクを簡素化します。このチュートリアルでは、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 build 17063以降に含まれています。以前のバージョンのWindowsでは、インストールする必要がある場合があります。詳細については、TarとCurlが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ファイルを作成します。モジュールパスを使用したモジュールの名前付けの詳細については、依存関係の管理を参照してください。
次に、データを処理するためのデータ構造を設計します。
データを作成する
チュートリアルを簡単にするために、データをメモリに保存します。より一般的な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ステータスコードです。ここでは、
net/http
パッケージのStatusOK
定数を渡して、200 OK
を示しています。Context.IndentedJSON
をContext.JSON
の呼び出しに置き換えて、よりコンパクトな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.2
Goは、前の手順で追加した
import
宣言を満たすために、この依存関係を解決してダウンロードしました。 -
main.goを含むディレクトリのコマンドラインから、コードを実行します。ドット引数を使用して、「現在のディレクトリにあるコードを実行する」という意味で使用します。
$ go run .
コードが実行されると、リクエストを送信できる実行中のHTTPサーバーが起動します。
-
新しいコマンドラインウィンドウから、
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
リクエストを処理してアイテムを追加するコードを使用して、別のエンドポイントを作成します。
新しいアイテムを追加するハンドラを作成する
クライアントが/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
ステータスコードを追加します。
-
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
を使用して実行中の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 }
-
前のセクションと同様に、
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
パスパラメータを取得します。このハンドラーをパスにマッピングするとき、パスにパラメータのプレースホルダーを含めます。 -
スライス内の
album
構造体をループ処理し、ID
フィールドの値がid
パラメータ値と一致するものを探します。見つかった場合は、そのalbum
構造体をJSONにシリアライズし、200 OK
HTTPコードとともにレスポンスとして返します。上記のように、現実のサービスでは、この検索を実行するためにデータベースクエリを使用する可能性が高いでしょう。
-
アルバムが見つからない場合は、
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
を使用して実行中のWebサービスにリクエストを行います。$ curl http://localhost:8080/albums/2
コマンドは、使用したIDのアルバムのJSONを表示します。アルバムが見つからなかった場合は、エラーメッセージを含むJSONが表示されます。
{ "id": "2", "title": "Jeru", "artist": "Gerry Mulligan", "price": 17.99 }
結論
おめでとうございます!GoとGinを使用して、シンプルなRESTful Webサービスを作成しました。
次の推奨トピック
- Goを初めて使用する場合は、Effective GoとHow to write Go codeで説明されている便利なベストプラクティスが見つかります。
- Go Tourは、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"})
}