「Golang」 ×「gorilla/mux router」でシンプルなREST APIサーバーを開発する

# <font color="LightSeaGreen">Why Golang?</font>
`Golang`の特徴として「シンプル」「静的型付けのため高速」「マルチプロセッシングによる並列処理」があります。

このような特徴から`Docker`や`Kubernetes`などの高速処理を要求されるインフラ基盤構築サービスでも使われており、またイーサリアムの`Geth`など「ブロックチェーン」の基盤として活用されているケースもあるようです。

また、`C言語`をベースに開発された言語のため、「構造体」「ポインタ」「チャネル」など普段`Typescript`や`Python`などのコードを書いていると若干とっつきにくさはあるのですが、C言語よりは数段理解がしやすく、短い記述で処理を書くことができます。
（私自身、C言語で挫折した経験があったので、若干不安ではありましたがコードがシンプルなため他の言語の経験があれば学習コストは非常に低いと思います。）


**まとめると「比較的学習コストが低い」かつ「高速」...　最高ですね！**


`Google`→`Golang`ときたら`gRPC`だろと突っ込まれるかもしれませんが、今回は基礎をおさえる意味で単純な`REST API`で実装を行います。

# <font color="LightSeaGreen">何を作るか？</font>
今回、ベタですが「商品管理システム」を開発すると仮定して、商品マスタより商品の閲覧・登録・削除・更新ができるような仕様を想定しています。

とりあえずは`REST API`の機能に注力したいので、データベースは使わずシステム内で一時的に保持するような仕組みで作成します。

本来であれば、「生産地」や「工場情報」などを入れるべきですが、今回は構成をシンプルにするために最低限の要素に絞っています。また、カテゴリIDなどのリレーションシップも今回は省き、1テーブルのみで機能を実装します。

| Properties　　　 |   Types　　　　      | Summaries　　　　 |
|----------|:-------------|:------|
| id |  integer | 商品ID |
| jan_code |    string   |   JANコード |
| item_name |    string   |   商品名 |
| price | integer |  価格 |
| category_id | integer |    カテゴリID |
| series_id | integer |    シリーズID |
| stock | integer |    在庫数 |
| discontinued | boolean |    廃番 |
| release_date | datetime |    発売日 |
| created_at | datetime |    作成日 |
| updated_at | datetime |    更新日 |
| deleted_at | datetime |    削除日 |

# <font color="LightSeaGreen">コード実装</font>

## バージョン情報

- go 1.14
- [github.com/gorilla/mux v1.7.4](https://github.com/gorilla/mux)

## HTTPルーター
`Gin`などのフルスタックフレームワークの導入も考えましたが、`REST API`を作成するのに最低限の要素にしたかったのでルーター機能だけを提供する`gorilla/mux`を採用しました。
これは好みの問題ですが、個人的に`React`のように「必要なときに必要な分だけ」みたいなパッケージの導入が一番ベストかなと考えています。


それでは早速`gorilla/mux`を[go modules](https://github.com/golang/go/wiki/Modules
)を使って導入していきます。
`go module`を使うと、システム内で使われているパッケージを管理できるうえ、ビルド時に依存パッケージを自動インストールできます。（便利！）

```shell:shellです
go mod init { プロジェクト名 }
go get github.com/gorilla/mux
```

`go.mod`・`go.sum`ファイルが自動生成されます。

```golang:go.mod
module { プロジェクト名 }

go 1.14

require (
	github.com/gorilla/mux v1.7.4
)
```


## フォルダ構成

MVCモデルです。ルートに配置した`main.go`ファイルからコントローラを呼び出し、サーバを起動します。

```:フォルダ構成
merchandise_control_system
├── controllers
│   └── webserver.go
├── go.mod
├── go.sum
└── main.go

```



## コード全体

```golang:main.go
package main

// go modulesで管理すると絶対パスでのimportが必須です
import (
	"merchandise_control_system/controllers"
)

func main() {
	controllers.StartWebServer()
}
```

```golang:controllers/webserver.go
package controllers

import (
	"encoding/json"
	"fmt"
	"github.com/gorilla/mux"
	"io/ioutil"
	"log"
	"net/http"
	"time"
)

// json: ~ をパラメータに付与すると、jsonエンコード時にパラメータ名を指定することができます。
// また、omitemptyを付与するとパラメータが空のときに、jsonのパラメータから消すことができます。
// これはクライアントアプリと仕様を統一する必要があります。
type ItemParams struct {
	Id           string    `json:"id"`
	JanCode      string    `json:"jan_code,omitempty"`
	ItemName     string    `json:"item_name,omitempty"`
	Price        int       `json:"price,omitempty"`
	CategoryId   int       `json:"category_id,omitempty"`
	SeriesId     int       `json:"series_id,omitempty"`
	Stock        int       `json:"stock,omitempty"`
	Discontinued bool      `json:"discontinued"`
	ReleaseDate  time.Time `json:"release_date,omitempty"`
	CreatedAt    time.Time `json:"created_at"`
	UpdatedAt    time.Time `json:"updated_at"`
	DeletedAt    time.Time `json:"deleted_at"`
}

// ポインタ型でitemsを定義します。今回はこのグローバル変数【配列】がデータベースの役割をします
var items []*ItemParams

func rootPage(w http.ResponseWriter, r *http.Request) {
	fmt.Fprintf(w, "Welcome to the Go Api Server")
	fmt.Println("Root endpoint is hooked!")
}

func fetchAllItems(w http.ResponseWriter, r *http.Request) {
	w.Header().Set("Content-Type", "application/json")
	json.NewEncoder(w).Encode(items)
}

func fetchSingleItem(w http.ResponseWriter, r *http.Request) {
	w.Header().Set("Content-Type", "application/json")
	vars := mux.Vars(r)
	key := vars["id"]

	for _, item := range items {
		if item.Id == key {
			json.NewEncoder(w).Encode(item)
		}
	}
}

func createItem(w http.ResponseWriter, r *http.Request) {
	reqBody, _ := ioutil.ReadAll(r.Body)
	var item ItemParams
	if err := json.Unmarshal(reqBody, &item); err != nil {
		log.Fatal(err)
	}

	items = append(items, &item)
	json.NewEncoder(w).Encode(item)
}

func deleteItem(w http.ResponseWriter, r *http.Request) {
	vars := mux.Vars(r)
	id := vars["id"]

	for index, item := range items {
		if item.Id == id {
			items = append(items[:index], items[index+1:]...)
		}
	}
}

func updateItem(w http.ResponseWriter, r *http.Request) {
	vars := mux.Vars(r)
	id := vars["id"]

	reqBody, _ := ioutil.ReadAll(r.Body)
	var updateItem ItemParams
	if err := json.Unmarshal(reqBody, &updateItem); err != nil {
		log.Fatal(err)
	}

	for index, item := range items {
		if item.Id == id {
			items[index] = &ItemParams{
				Id:           item.Id,
				JanCode:      updateItem.JanCode,
				ItemName:     updateItem.ItemName,
				Price:        updateItem.Price,
				CategoryId:   updateItem.CategoryId,
				SeriesId:     updateItem.SeriesId,
				Stock:        updateItem.Stock,
				Discontinued: updateItem.Discontinued,
				ReleaseDate:  updateItem.ReleaseDate,
				CreatedAt:    item.CreatedAt,
				UpdatedAt:    updateItem.UpdatedAt,
				DeletedAt:    item.DeletedAt,
			}
		}
	}
}

// 先頭を「大文字」にすると外部ファイルから読み込めるようになります。（export）
func StartWebServer() error {
	fmt.Println("Rest API with Mux Routers")
	router := mux.NewRouter().StrictSlash(true)

	// router.HandleFunc({ エンドポイント }, { レスポンス関数 }).Methods({ リクエストメソッド（複数可能） })
	router.HandleFunc("/", rootPage)
	router.HandleFunc("/items", fetchAllItems).Methods("GET")
	router.HandleFunc("/item/{id}", fetchSingleItem).Methods("GET")

	router.HandleFunc("/item", createItem).Methods("POST")
	router.HandleFunc("/item/{id}", deleteItem).Methods("DELETE")
	router.HandleFunc("/item/{id}", updateItem).Methods("PUT")

	return http.ListenAndServe(fmt.Sprintf(":%d", 8080), router)
}

// モックデータを初期値として読み込みます
func init() {
	items = []*ItemParams{
		&ItemParams{
			Id:           "1",
			JanCode:      "327390283080",
			ItemName:     "item_1",
			Price:        2500,
			CategoryId:   1,
			SeriesId:     1,
			Stock:        100,
			Discontinued: false,
			ReleaseDate:  time.Now(),
			CreatedAt:    time.Now(),
			UpdatedAt:    time.Now(),
			DeletedAt:    time.Now(),
		},
		&ItemParams{
			Id:           "2",
			JanCode:      "3273902878656",
			ItemName:     "item_2",
			Price:        1200,
			CategoryId:   2,
			SeriesId:     2,
			Stock:        200,
			Discontinued: false,
			ReleaseDate:  time.Now(),
			CreatedAt:    time.Now(),
			UpdatedAt:    time.Now(),
			DeletedAt:    time.Now(),
		},
	}
}

```

# <font color="LightSeaGreen">デモ</font>
APIサーバを起動します。

```shell
go run main.go
```

`Postman`からリクエストを送ってみます。（curlコマンドでもOKです）

### 【GET】 /items
![f899b6d09ac1bc86c96424ab33fd5756.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/498345/2a1152ba-2a90-d6ae-5c9e-a709794db84a.png)

### 【GET】 /item/1
![116bc4a421560f04cfb02a9b38327e75.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/498345/e7f38885-ed70-02ef-1234-5d6ca331e161.png)

### 【POST】 /item
![1fc1dd1fc422be364916f0181e16b3bb.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/498345/cc6ec6ea-1f8f-1b6a-8b8f-9e6b18b7feb6.png)

### 【DELETE】 /item/1
![5a6ef4e714f695d6e6ab5c6cb0bb9ebc.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/498345/1b76e9bd-2321-965b-0304-9ef9c7fd03d1.png)

![33e7b45ab8ab23ec55c3931235dbad53.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/498345/be649972-d1b5-b868-b659-ba0de3af759c.png)

### 【PUT】 /item/2
![700480fb85e54d8aabd5565e51b775c8.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/498345/a811f060-e5e3-55c5-9a64-496743b09922.png)
![8b854b697612094b36a382f0ec5435cd.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/498345/98be2a5d-6376-f2aa-26ed-fa691e19d3df.png)




# <font color="LightSeaGreen">まとめ</font>

今回はコントローラ部分のみで、とりあえず動くように実装してみました。
エラーハンドリングは甘々ですので、改善ポイントは多くあると思います。`Golang`はエラーハンドリングを行う部分が多く、どこまで徹底するのか判断が難しい気がしますね...

C言語をはじめ、静的型付け言語はどうしても難しそうという抵抗感がありますが、「安全」「高速」などメリットも多いかと思います。
Go言語は深く極めるのは難しいのですが、「とりあえず動くものを作る」ハードルは低く、手軽に試せるのかなという印象を受けました。


また、今回の記事では実装しませんでしたが、以下内容も開発予定です。

- 「モデル」部分を`Gorm`を利用した`Mysql`へのデータベース保存（以下URLに、記事を投稿いたしました）
 - [「Golang」 ×「Gorm」でシンプルに「Mysql」を操作する](https://qiita.com/stranger1989/items/914bef8cc6ad77ef8350)
- 「ビュー」の部分は切り離して`ReactNative`で別途モバイルアプリ開発

開発が終わり次第、こちらも記事にしていきたいと思います。

# <font color="LightSeaGreen">参考サイト</font>


- [Creating a RESTful API With Golang](https://tutorialedge.net/golang/creating-restful-api-with-golang/) 
- [現役シリコンバレーエンジニアが教えるGo入門 + 応用でビットコインのシストレFintechアプリの開発](https://www.udemy.com/course/go-fintech/)