【gin】goでAPIサーバー

# golangでAPIサーバー
[gin](https://github.com/gin-gonic/gin)というフレームワークを使ってAPIサーバーを建て、CRUD処理を実装します。
プログラム全体は[こちら](https://github.com/shuyaeer/go_api_server)においてあります。

## 環境
MacBook Pro (14-inch, 2021)
Go 1.18.2
gin 1.77

## APIの設計
焼き鳥屋の商品情報を扱うものを想定します。
 - 商品一覧の取得
 - 単一商品の取得
 - 新商品の登録
 - 在庫情報の変更
 - 商品の削除

これらを実装します。

## ディレクトリとファイル類を作成
```bash
$ mkdir go_api_server
$ cd go_api_sever
$ touch main.go # プログラムを書くファイルを作成
$ go mod init go_api_server # モジュールを管理するファイルを作成
$ go mod tidy
```

使用する外部モジュールであるginもここでインストールしておきます。

```bash
$ go get "github.com/gin-gonic/gin"
```
## READ
 商品一覧の取得
 - エンドポイント: /menu
 - メソッド: GET
 
`main.go`に処理を書いていきます。
リクエスト対して返却するJSONの構造を定義します。
各商品は以下の4つの情報を持ちます。
 - Id
 - Name
 - Price
 - Stock

```go
type menu struct {
	Id    int    `json:"id"`
	Name  string `json:"name"`
	Price int    `json:"price"`
	Stock bool   `json:"stock"`
}

```

この定義した`menu`という構造体に予めいくつかの商品データを登録しておきます。

```go
var menuList = []menu{
	{Id: 1, Name: "皮", Price: 120, InStock: true},
	{Id: 2, Name: "レバー", Price: 150, InStock: true},
	{Id: 3, Name: "ぼんじり", Price: 180, InStock: true},
}
```

リクエストに対してjsonを返却する関数です。

```go
func getMenuList(c *gin.Context) {
	c.IndentedJSON(http.StatusOK, menuList)
}
```

main関数内でルーティングやサーバーの起動を行います。

```go
func main() {
	router := gin.Default()
	router.GET("/menu", getMenuList) // ルーティング
	router.Run("localhost:8080")
}

```

一度ここでサーバーを起動してみましょう。

```bash
$ go run main.go
```
起動できたら、ターミナルで`GET`リクエストをしてみます。

```bash
$ curl http://localhost:8080/menu
[
    {
        "id": 1,
        "name": "皮",
        "price": 120,
        "instock": true
    },
    {
        "id": 2,
        "name": "レバー",
        "price": 150,
        "instock": true
    },
    {
        "id": 3,
        "name": "ぼんじり",
        "price": 180,
        "instock": true
    }
]
```

期待通り取得できました。

### 単一商品の取得
 - エンドポイント: /menu/:id
 - メソッド: GET 
線形的に探索して、idが一致する商品を返却します。

```go
func getItemById(c *gin.Context) {
	id := c.Param("id")
	i, _ := strconv.Atoi(id)
	for _, a := range menuList {
		if a.Id == i {
			c.IndentedJSON(http.StatusOK, a)
			return
		}
	}
	c.IndentedJSON(http.StatusNotFound, gin.H{"message": "Item not found"})
}
```
```go
func main() {
	router := gin.Default()
	router.GET("/menu", getMenuList)
	router.GET("/menu/:id", getItemById) // new
	router.Run("localhost:8080")
}
```
```bash
$ curl http://localhost:8080/menu/1
{
    "id": 1,
    "name": "皮",
    "price": 120,
    "instock": true
}
```

## CREATE
新商品のを登録を実装。
- エンドポイント: /menu
- メソッド: POST

```bash
{"id": 4, "name": "はつもと", "price": 200, "instock": true}
```

新たな商品を登録する関数を定義します。

```go
func postItem(c *gin.Context) {
	var newMenu menu
	err := c.BindJSON(&newMenu)
	if err != nil {
		return
	}
	menuList = append(menuList, newMenu) // 追加処理
	c.IndentedJSON(http.StatusCreated, newMenu)
}

```

main関数に新たなルーティングの記述を追加します。

```go
func main() {
	router := gin.Default()
	router.GET("/menu", getMenuList)
    router.GET("/menu/:id", getItemById)
	router.POST("/menu", postItem) // new
	router.Run("localhost:8080")
}
```

再度サーバーを起こして以下を叩いてみましょう。

```bash
$ curl http://localhost:8080/menu -X POST -H "Content-Type: application/json" \
   --data '{"id": 4, "name": "はつもと", "price": 200, "instock": true}'
```

登録された商品のデータが返却されました。

```bash
{
    "id": 4,
    "name": "はつもと",
    "price": 200,
    "instock": true
}
```

登録されたか確認するために商品一覧を取得してみます。

```bash
$ curl http://localhost:8080/menu
[
    {
        "id": 1,
        "name": "皮",
        "price": 120,
        "instock": true
    },
    {
        "id": 2,
        "name": "レバー",
        "price": 150,
        "instock": true
    },
    {
        "id": 3,
        "name": "ぼんじり",
        "price": 180,
        "instock": true
    },
    {
        "id": 4,
        "name": "はつもと", # new
        "price": 200,
        "instock": true
    }
]
```
はつもとが登録されているのが確認できます。

## UPDATE
在庫情報の変更
- エンドポイント: /menu/outofstock/:id
- メソッド: GET
レバーが品切れになってしまいました。`instock`を`false`に更新します。

```go
func updateStockById(c *gin.Context) {
	id := c.Param("id")
	i, _ := strconv.Atoi(id)
	for _, a := range menuList {
         // inStockを反転
		if a.Id == i {
			stock := a.InStock
			if stock == false {
				a.InStock = true
			} else {
				a.InStock = false
			}
			c.IndentedJSON(http.StatusOK, a)
			return
		}
	}
	c.IndentedJSON(http.StatusNotFound, gin.H{"message": "Item not found"})
}
```

```go
func main() {
	router := gin.Default()
	router.GET("/menu", getMenuList)
	router.GET("/menu/:id", getItemById)
	router.POST("/menu", postItem)
	router.GET("/menu/outofstock/:id", updateStockById) // new
	router.Run("localhost:8080")
}
```

確認します
```bash
$ curl http://localhost:8080/menu/outofstock/2
{
    "id": 2,
    "name": "レバー",
    "price": 150,
    "instock": false
}
```

## DELETE
商品の削除
 - エンドポイント: /menu/delete:id
 - メソッド: GET

ボンジリを一覧から削除します。

```go
func DeleteById(c *gin.Context) {
	id := c.Param("id")
	i, _ := strconv.Atoi(id)
	for j, a := range menuList {
		if a.Id == i {
			menuList = append(menuList[:j], menuList[j+1:]...) // 削除部分
			c.IndentedJSON(http.StatusOK, a)
			return
		}
	}
	c.IndentedJSON(http.StatusNotFound, gin.H{"message": "Item not found"})
}
```

```go
func main() {
	router := gin.Default()
	router.GET("/menu", getMenuList)
	router.GET("/menu/:id", getItemById)
	router.POST("/menu", postItem)
	router.GET("/menu/outofstock/:id", updateStockById)
	router.GET("/menu/delete/:id", DeleteById) // new
	router.Run("localhost:8080")
}
```
idが3の商品を削除します

```bash
$ curl http://localhost:8080/menu/delete/3
{
    "id": 3,
    "name": "ぼんじり",
    "price": 180,
    "instock": true
}
```

一覧から削除されていることを確認します。
サーバーを起こし直したので、はつもとが無いのは期待通りです。

```bash
$ curl http://localhost:8080/menu
[
    {
        "id": 1,
        "name": "皮",
        "price": 120,
        "instock": true
    },
    {
        "id": 2,
        "name": "レバー",
        "price": 150,
        "instock": true
    }
]
```

# おわりに
とっても簡単にAPIサーバーの構築ができました。次はDBにデータを登録できるよう実装します。