こんにちは。KOUKIです。
とあるWeb系企業で、Goエンジニアをやっています。
前回は、Go言語のフレームワークである「Gin」を使って簡単なAPI開発手法を紹介しました。
今回はより実践的な内容として、CRUD機能を取り込んだ「レシピを取り扱うAPI(recipe-api)」を実装していきたいと思います。
<目次>
参考
以下のリポジトリを参考にしています。
https://github.com/PacktPublishing/Building-Distributed-Applications-in-Gin
環境
Mac + Chromeで開発します。
前回
前回は、Hello アプリを開発しました。
プロジェクトの作成
プロジェクトを作成しましょう。
1 2 3 4 5 6 7 |
mkdir recipe-api cd recipe-api touch main.go go mod init recipe-api go get github.com/gin-gonic/gin go get github.com/rs/xid |
main.goに最低限のコードを書きます。
1 2 3 4 5 6 7 8 9 |
// main.go package main import "github.com/gin-gonic/gin" func main() { router := gin.Default() router.Run() } |
Recipeモデルの定義
Recipeモデルを定義しましょう。
まずは、必要なディレクトリとファイルを用意します。
1 2 |
mkdir models touch models/recipe.go |
次に、GoのStructでモデルを実装します。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
// models/recipe.go package models import "time" type Recipe struct { ID string `json:"id"` Name string `json:"name"` Tags []string `json:"tags"` Ingredients []string `json:"ingredients"` Instructions []string `json:"instructions"` PublishedAt time.Time `json:"publishedAt"` } |
このModelにAPIのリクエスト/レスポンスデータを格納します。
jsonタグはレスポンスを返すときに、Keyをいい感じに整形してくれます。
1 2 3 4 5 |
{ "name": "XXXX", "tags" : "XXXX", <--------------- jsonタグ名(tags)と同じものが名前が返却される ... } |
HTTPエンドポイントの実装
下記に示すHTTPエンドポイントを実装しましょう。
HTTP Method | リソース | Description |
---|---|---|
GET | /recipes | レシピの一覧を返す |
POST | /recipes | 新しいレシピを作る |
PUT | /recipes/{id} | レシピをアップデートする |
DELETE | /recipes | レシピを削除する |
GET | /recipes/search?tag=X | Tagに指定されたレシピを返却する |
HTTPルートを実装
HTTPエンドポイントを参考に、ルートを実装していきましょう。
以下のディレクトリとファイルを用意しておきます。
1 2 |
mkdir routes touch routes/route.go |
レシピの作成(recipes): POST
1 2 |
mkdir handlers touch handlers/newrecipehandler.go |
まず、handlerを実装しましょう。ここにはレシピを作成するための処理を実装します。
1 2 3 4 5 6 7 8 |
// handlers/newrecipehandler.go package handlers import "github.com/gin-gonic/gin" func NewRecipeHandler(c *gin.Context) { // Todo } |
中身は後で実装するので、とりあえず先に進みましょう。
次は、ルートを実装します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
// routes/route.go package routes import ( "recipe-api/handlers" "github.com/gin-gonic/gin" ) func NewRoutes() *gin.Engine { router := gin.Default() // レシピ作成 router.POST("/recipes", handlers.NewRecipeHandler) return router } |
ここには、GETやDELETEなど他のHTTPエンドポイントを追加していく予定です。
次に、routeをmain関数から呼び出しましょう。
1 2 3 4 5 6 7 8 9 |
// main.go package main import "recipe-api/routes" func main() { router := routes.NewRoutes() router.Run() } |
いい感じですね。NewRecipeHandlerの中身を実装しましょう。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 |
// handlers/newrecipehandler.go package handlers import ( "net/http" "recipe-api/models" "time" "github.com/gin-gonic/gin" "github.com/rs/xid" ) // 一時的なtmpDB // 将来的にはDatabaseに格納したい var recipes []models.Recipe func init() { recipes = make([]models.Recipe, 0) } func NewRecipeHandler(c *gin.Context) { var recipe models.Recipe // リクエストデータを取り出す if err := c.ShouldBindJSON(&recipe); err != nil { c.JSON(http.StatusBadRequest, gin.H{ "error": err.Error()}) return } // ユニークなIDを生成 recipe.ID = xid.New().String() // 現在時刻を追加 recipe.PublishedAt = time.Now() recipes = append(recipes, recipe) c.JSON(http.StatusOK, recipe) } |
recipes変数は一時的なデータ格納場所です。将来はDatabaseにデータを保存したいですね。
またGinに限らず大抵のフレームワークには、データバインディング機能(c.ShouldBindJSON(&recipe))がついてます。
これは、Goの構造体とリクエストの構造体(ここではJSON)の間で一致したパラメーターをrecipe変数にコピーしてくれます。大変便利です。
テストをしてみましょう。Terminal上で以下のコマンドを実行ください。
1 2 3 4 5 6 7 8 9 10 |
$ go run main.go [GIN-debug] [WARNING] Creating an Engine instance with the Logger and Recovery middleware already attached. [GIN-debug] [WARNING] Running in "debug" mode. Switch to "release" mode in production. - using env: export GIN_MODE=release - using code: gin.SetMode(gin.ReleaseMode) [GIN-debug] POST /recipes --> recipe-api/handlers.NewRecipeHandler (3 handlers) [GIN-debug] Environment variable PORT is undefined. Using port :8080 by default [GIN-debug] Listening and serving HTTP on :8080 |
私はテストAPIツールとして、ChromeのTalend APIを利用します。
これを使って、以下のパラメーターでAPIへリクエストを送ってみましょう。
- URL: http://localhost:8080/recipes
- 形式: POST
- Body: ↓
1 2 3 4 5 6 7 8 9 |
{ "name": "Homemade Pizza", "tags" : ["italian", "pizza", "dinner"], "ingredients": [ "1 1/2 cups (355 ml) warm water (105°F-115°F)", "1 package (2 1/4 teaspoons) of active dry yeast", "3 3/4 cups (490 g) bread flour", "feta cheese, firm mozzarella cheese, grated" ], "instructions": [ "Step 1.", "Step 2.", "Step 3." ] } |


200 OKかつ投入データが確認できたのでOKですね。
レシピ一覧の取得(recipes): GET
続いて、レシピ一覧を取得しましょう。
Handlerを作成します。
1 |
touch handlers/listrecipeshandler.go |
1 2 3 4 5 6 7 8 9 10 11 12 |
// handlers/listrecipeshandler.go package handlers import ( "net/http" "github.com/gin-gonic/gin" ) func ListRecipesHandler(c *gin.Context) { c.JSON(http.StatusOK, recipes) } |
これは単純に、先ほど作成したrecipes(一時的なデータベース)を呼び出し元に返しているだけです。
ListRecipesHandlerをルートに加えます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
// routes/route.go package routes import ( "recipe-api/handlers" "github.com/gin-gonic/gin" ) func NewRoutes() *gin.Engine { router := gin.Default() // レシピ作成 router.POST("/recipes", handlers.NewRecipeHandler) // レシピ一覧の取得 router.GET("/recipes", handlers.ListRecipesHandler) return router } |
テストをしてみましょう。
Ctrl + cで先ほど立ち上げたWebサーバーを切ってから「go run main.go」で立ち上げ直してください。
Webサーバーを立ち上げ直すと先ほど追加したレシピは消えてしまうので(一時的なものなので)、レシピを入れ直してから以下のパラメータでデータの取得を試してみましょう。
- URL: http://localhost:8080/recipes
- 形式: GET

OKですね。
初期データの投入
Webサーバーを起動しなす度に投入したデータが消えるのはめんどくさいですね。
Webサーバー起動時に、初期データを投入できるようにしましょう。
まず、JSONでデータを作成します。
1 |
touch recipes.json |
このファイルに以下のデータを追加します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
[ { "id": "dfjpaidhjfpiajdpif", "name": "Homemade Pizza", "tags": [ "italian", "pizza", "dinner" ], "ingredients": [ "1 1/2 cups (355 ml) warm water (105°F-115°F)", "1 package (2 1/4 teaspoons) of active dry yeast", "3 3/4 cups (490 g) bread flour", "feta cheese, firm mozzarella cheese, grated" ], "instructions": [ "Step 1.", "Step 2.", "Step 3." ] } ] |
newrecipehandler.goファイルのinitに「ioutil.ReadFile」メソッドを使って先ほど作成したJSONファイルを読み込む処理を追加します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
// handlers/newrecipehandler.go package handlers import ( "encoding/json" "io/ioutil" "net/http" "recipe-api/models" "time" "github.com/gin-gonic/gin" "github.com/rs/xid" ) ... func init() { recipes = make([]models.Recipe, 0) file, _ := ioutil.ReadFile("recipes.json") _ = json.Unmarshal([]byte(file), &recipes) } |
これで、初期データの投入は完了です。Webサーバーを起動し直してGETリクエストでデータを取得してみてください。データが取得できるはずです。

CURLでも試せます。※ jqを使っています
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
$ curl -s -X GET 'localhost:8080/recipes' | jq . [ { "id": "dfjpaidhjfpiajdpif", "name": "Homemade Pizza", "tags": [ "italian", "pizza", "dinner" ], "ingredients": [ "1 1/2 cups (355 ml) warm water (105°F-115°F)", "1 package (2 1/4 teaspoons) of active dry yeast", "3 3/4 cups (490 g) bread flour", "feta cheese, firm mozzarella cheese, grated" ], "instructions": [ "Step 1.", "Step 2.", "Step 3." ], "publishedAt": "0001-01-01T00:00:00Z" } ] |
レシピ一覧の更新(recipes/id): PUT
続いて、レシピを更新する処理を実装します。
例によって、ハンドラーを作成します。
1 |
touch handlers/updaterecipeshandler.go |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 |
// handlers/updaterecipeshandler.go package handlers import ( "net/http" "recipe-api/models" "github.com/gin-gonic/gin" ) func UpdateRecipeHandler(c *gin.Context) { // urlに指定するrecipes/:idからidを取得できる id := c.Param("id") var recipe models.Recipe // データバインディングを行う if err := c.ShouldBindJSON(&recipe); err != nil { // エラーの場合は、400エラーを返す c.JSON(http.StatusBadRequest, gin.H{ "error": err.Error()}) return } // 更新するレシピのIndexをrecipesから探す targetRecipeIndex := -1 for i := 0; i < len(recipes); i++ { if recipes[i].ID == id { targetRecipeIndex = i } } // もし更新するレシピがない場合 if targetRecipeIndex == -1 { c.JSON(http.StatusNotFound, gin.H{ "error": "Recipe not found"}) return } // 更新する recipes[targetRecipeIndex] = recipe c.JSON(http.StatusOK, recipe) } |
ginのParamメソッドはURLに記述された「:id」からIDを取得します。この「: + id」を所定のフォーマットになります。
例えば「:name」の場合は、c.Param(“name”)でデータを取り出すイメージです。
idを取得し、リクエストデータをrecipe変数にバインドしたら既存のデータ検索の実装に移ります。
既存のデータから今回リクエストしたidで検索をかけ、該当するデータをそっくりそのまま上書きする処理を実装しています。
Databaseを導入すればもっといい感じの処理がかけますが、今回はこれでよしとしましょう。
続いて、ルートにUpdateRecipeHandlerを追加しましょう。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
// routes/route.go package routes import ( "recipe-api/handlers" "github.com/gin-gonic/gin" ) func NewRoutes() *gin.Engine { router := gin.Default() // レシピ作成 router.POST("/recipes", handlers.NewRecipeHandler) // レシピ一覧の取得 router.GET("/recipes", handlers.ListRecipesHandler) // レシピの更新 router.PUT("/recipes/:id", handlers.UpdateRecipeHandler) return router } |
動作確認をします。Webサーバーを起動し直します。
1 2 3 4 |
# Webサーバーを落とす Ctrl + c # Webサーバーを起動する go run main.go |
現在の状態を確認するために、以下のコマンドを実行しましょう。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
$ curl -s -X GET 'localhost:8080/recipes' | jq . [ { "id": "dfjpaidhjfpiajdpif", "name": "Homemade Pizza", "tags": [ "italian", "pizza", "dinner" ], "ingredients": [ "1 1/2 cups (355 ml) warm water (105°F-115°F)", "1 package (2 1/4 teaspoons) of active dry yeast", "3 3/4 cups (490 g) bread flour", "feta cheese, firm mozzarella cheese, grated" ], "instructions": [ "Step 1.", "Step 2.", "Step 3." ], "publishedAt": "0001-01-01T00:00:00Z" } ] |
このデータの中で、nameを変更してみましょう。
以下のパラメーターで、データを送信します。
- URL: http://localhost:8080/recipes/dfjpaidhjfpiajdpif
- 形式: PUT
1 2 3 4 5 6 7 8 9 |
"name": "Homemade Pizza_Update", "tags" : ["italian", "pizza", "dinner"], "ingredients": [ "1 1/2 cups (355 ml) warm water (105°F-115°F)", "1 package (2 1/4 teaspoons) of active dry yeast", "3 3/4 cups (490 g) bread flour", "feta cheese, firm mozzarella cheese, grated" ], "instructions": [ "Step 1.", "Step 2.", "Step 3." ] } |
※ dfjpaidhjfpiajdpifがIDです


Updateされましたね。CURLでも確認します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
$ curl -s -X GET 'localhost:8080/recipes' | jq . [ { "id": "", "name": "Homemade Pizza_Update", "tags": [ "italian", "pizza", "dinner" ], "ingredients": [ "1 1/2 cups (355 ml) warm water (105°F-115°F)", "1 package (2 1/4 teaspoons) of active dry yeast", "3 3/4 cups (490 g) bread flour", "feta cheese, firm mozzarella cheese, grated" ], "instructions": [ "Step 1.", "Step 2.", "Step 3." ], "publishedAt": "0001-01-01T00:00:00Z" } ] |
レシピ一覧の削除(recipes/id): DELETE
次は、レシピを削除する処理を実装しましょう。
例によって、Handlerを実装します。
1 |
touch handlers/deleterecipehandler.go |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
// handlers/deleterecipehandler.go package handlers import ( "net/http" "github.com/gin-gonic/gin" ) func DeleteRecipeHandler(c *gin.Context) { // リクエストからIDを取得 id := c.Param("id") recipeIndex := -1 for i := 0; i < len(recipes); i++ { if recipes[i].ID == id { recipeIndex = i } } // レシピが見つからない場合 if recipeIndex == -1 { c.JSON(http.StatusNotFound, gin.H{ "error": "recipe not found"}) return } // idが一致したレシピ"以外"をrecipes変数に詰め直す recipes = append(recipes[:recipeIndex], recipes[recipeIndex+1:]...) c.JSON(http.StatusOK, gin.H{ "message": "Recipe has been deleted"}) } |
更新処理とロジックはほぼ同じです。
ルートも追加しましょう。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
// routes/route.go package routes import ( "recipe-api/handlers" "github.com/gin-gonic/gin" ) func NewRoutes() *gin.Engine { router := gin.Default() // レシピ作成 router.POST("/recipes", handlers.NewRecipeHandler) // レシピ一覧の取得 router.GET("/recipes", handlers.ListRecipesHandler) // レシピの更新 router.PUT("/recipes/:id", handlers.UpdateRecipeHandler) // レシピの削除 router.DELETE("/recipes/:id", handlers.DeleteRecipeHandler) return router } |
Webサーバーを起動し直してください。
まず、recipesを取得してみましょう。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
$ curl -X GET localhost:8080/recipes | jq -r % Total % Received % Xferd Average Speed Time Time Time Current Dload Upload Total Spent Left Speed 0 0 0 0 0 0 0 0 --:--:-- --:--:-- --:--:--100 367 100 367 0 0 91750 0 --:--:-- --:--:-- --:--:-- 91750 [ { "id": "dfjpaidhjfpiajdpif", "name": "Homemade Pizza", "tags": [ "italian", "pizza", "dinner" ], "ingredients": [ "1 1/2 cups (355 ml) warm water (105°F-115°F)", "1 package (2 1/4 teaspoons) of active dry yeast", "3 3/4 cups (490 g) bread flour", "feta cheese, firm mozzarella cheese, grated" ], "instructions": [ "Step 1.", "Step 2.", "Step 3." ], "publishedAt": "0001-01-01T00:00:00Z" } ] |
レシピは存在しています。
続いて、下記のコマンドでレシピを削除します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
$ curl -v -sX DELETE localhost:8080/recipes/dfjpaidhjfpiajdpif | jq -r * Trying 127.0.0.1... * TCP_NODELAY set * Connected to localhost (127.0.0.1) port 8080 (#0) > DELETE /recipes/dfjpaidhjfpiajdpif HTTP/1.1 > Host: localhost:8080 > User-Agent: curl/7.64.1 > Accept: */* > < HTTP/1.1 200 OK < Content-Type: application/json; charset=utf-8 < Date: Fri, 19 Nov 2021 22:09:28 GMT < Content-Length: 37 < { [37 bytes data] * Connection #0 to host localhost left intact * Closing connection 0 { "message": "Recipe has been deleted" } |
OKですね。
念の為、Recipeを取得してみましょう。
1 2 |
$ curl -X GET localhost:8080/recipes [] |
レシピ一覧の検索(recipes/search): GET
最後に、レシピを検索する処理を実装しましょう。
テストデータ(recipies.json)を増やしておきます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 |
[ { "id": "dfjpaidhjfpiajdpif", "name": "Homemade Pizza", "tags": [ "italian", "pizza", "dinner" ], "ingredients": [ "1 1/2 cups (355 ml) warm water (105°F-115°F)", "1 package (2 1/4 teaspoons) of active dry yeast", "3 3/4 cups (490 g) bread flour", "feta cheese, firm mozzarella cheese, grated" ], "instructions": [ "Step 1.", "Step 2.", "Step 3." ] }, { "id": "oijoijfoijdoaf", "name": "Homemade Pizza2", "tags": [ "italian", "pizza" ], "ingredients": [ "1 1/2 cups (355 ml) warm water (105°F-115°F)", "1 package (2 1/4 teaspoons) of active dry yeast", "3 3/4 cups (490 g) bread flour", "feta cheese, firm mozzarella cheese, grated" ], "instructions": [ "Step 1.", "Step 2.", "Step 3." ] } ] |
さて、例によってHandlerを追加しましょう。
1 |
touch handlers/searchrecipehandler.go |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
// handlers/searchrecipehandler.go package handlers import ( "net/http" "recipe-api/models" "strings" "github.com/gin-gonic/gin" ) func SearchRecipesHandler(c *gin.Context) { // リクエストからtagを取得する // ?tag=XXXXの様に指定するだけで値を取得可能 tag := c.Query("tag") listOfRecipes := make([]models.Recipe, 0) // レシピをループ for i := 0; i < len(recipes); i++ { found := false // レシピ内のタグを抽出 for _, t := range recipes[i].Tags { // タグに合致しているか判定 if strings.EqualFold(t, tag) { found = true } } if found { listOfRecipes = append(listOfRecipes, recipes[i]) } } c.JSON(http.StatusOK, listOfRecipes) } |
GinのQueryメソッドは大変便利です。
例えば、「http://localhost:8080/recipes/search?tag=”orange”」とリクエストした場合、orangeを取得できます。
「?query名」のフォーマットですね。
実装したHandlerをルートに追加しましょう。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
// routes/route.go package routes import ( "recipe-api/handlers" "github.com/gin-gonic/gin" ) func NewRoutes() *gin.Engine { router := gin.Default() // レシピ作成 router.POST("/recipes", handlers.NewRecipeHandler) // レシピ一覧の取得 router.GET("/recipes", handlers.ListRecipesHandler) // レシピの更新 router.PUT("/recipes/:id", handlers.UpdateRecipeHandler) // レシピの削除 router.DELETE("/recipes/:id", handlers.DeleteRecipeHandler) // レシピの検索 router.GET("/recipes/search", handlers.SearchRecipesHandler) return router } |
Webサーバーを起動し直して、テストしましょう。
- URL: http://localhost:8080/recipes/search?tag=dinner
- 形式: GET


dinnerタグは、Homemade Pizzaにしかないのでそれしか取得できてません。
一応、共通するタグでリクエストを送ってみましょう。
- URL: http://localhost:8080/recipes/search?tag=italian

ちょっと見にくいですが、今回はどちらも取得できたことがわかります。
次回
次回は、OpenAPI(Swagger)を学習しましょう。
コメントを残す
コメントを投稿するにはログインしてください。