gin,gormを使ってgolangでRailsっぽい構成のAPIを作った 


#概要
- 仕事で`golang`を使ってみようという話になりAPIを作りました。
- 自分自身も他メンバーも`Rails`使いが多いのでRailsに寄せてMVC構成で作りました。
- `golang`に慣れたメンバーが少なかったのでできるだけ[メジャーで情報が多そうなフレームワーク](https://qiita.com/yumin/items/5de33b068ead564ebcbf)を使おうと考えて`gin`を採用しました。
- 同様にORMも`gorm`がメジャーなようだったので採用しました。
- `golang`自体が手探りの状態だったので主に以下を参考にさせて頂きました。
    - 日本語のQiitaの記事[Gin と GORM で作るオレオレ構成 API](https://qiita.com/Asuforce/items/0bde8cabb30ac094fcb4)
    - Starが多かった[深センの方が作ったAPI](https://github.com/eddycjy/go-gin-example)
- 自分でもサンプルAPIとして[gin-gorm-rails-like-sample-api](https://github.com/whisky-shusuky/gin-gorm-rails-like-sample-api)を作ってみましたので紹介のためにこの記事を書きました。
    - 想定読者として自分のように`Rails`辺りで開発してきて`golang`に手を出してみようと考えている方です。参考になりましたら幸いです。


# 全体構成
```
├── README.md
├── config
│   ├── config.go
│   └── development.yml
├── controller
│   ├── book_controller.go
│   └── shop_controller.go
├── db
│   └── db.go
├── docker
│   └── go
│       └── Dockerfile
├── docker-compose.yml
├── etc
│   └── development
│       ├── mysql
│       │   └── my.cnf
│       └── nginx.conf.d
│           └── routing.conf
├── go.mod
├── go.sum
├── main.go
├── model
│   ├── book.go
│   ├── entity
│   │   ├── book.go
│   │   └── shop.go
│   └── shop.go
├── server
│   └── server.go
├── sql
│   └── mysql
│       └── seed.sql
└── view
    ├── book.go
    ├── convert_function.go
    ├── schemas.go
    └── shop.go

```
- 基本的に参考にさせて頂いた[オレオレ構成 API](https://qiita.com/Asuforce/items/0bde8cabb30ac094fcb4)と同じです。
    - 大きく違う点としては`docker-compose.yml`など`docker`関連のファイルが追加されていることです。
        - これにより`sudo docker-compose up`で`nginx`,`mysql`,`go`のコンテナが立ち上がり動作させることができます(詳細な使用方法は[GithubのREADMEに記載しています](https://github.com/whisky-shusuky/gin-gorm-rails-like-sample-api))
            - ローカルでコンテナ上で開発することでスムーズにAWSの`FARGATE`にデプロイすることができました。([ちなみにデプロイ時のことはこちらの記事に記載しました](https://qiita.com/whisky-shusuky/items/a251af8e15ea41b8fd6d))
    - 他に違う点としてビジネスロジックを書く場所を`service`から`model`に置き換えている点ですね。
        - 概要に書いた通り`Rails`を使っているメンバーが多いので名称も合わせました。
        - また`model`の中でリソース毎に細かくパッケージを分けずに一つの`model`パッケージにまとめました。
            - パッケージをまとめることで特に問題が発生せず、また循環参照が発生しにくくなるというメリットが会ったためです。
                - 逆にまとめることのデメリットがよく分かっていないためコメント欄でご指摘頂けると幸いです。
        - また`view`も追加しています。
            - 後で見ますが`model`ではsqlの結果を取ることに留めて置いて`view`で実際に返却するjsonの値を設定しています。
            - Railsの`view`に当たる箇所で有り責務を分解できたかなと思います。
                - 「このAPIの戻り値ではこのキーはいらない！」といった要求がクライアントサイドから会ったときにAPI単位で柔軟に対応できるメリットがあると思います。

- 以下では内容を抜粋しながら細かく見ていきます。

## Main: main.go
```golang
func main() {
	db.Init()
	server.Init()
	db.Close()
}

```
- `go`のコンテナからこのファイルが実行されます。
- DB(`mysql`コンテナ)への接続情報が記載された`db`とルーティングが記載された`server`が実行されます。

## DB接続: db.go

```go
func Init() {
	configs, err := config.GetConfigs()
	if err != nil {
		panic(err)
	}

	Db, err = gorm.Open(configs.Database.Dialect, configs.Database.DataSource)
	Db.LogMode(true)
	if err != nil {
		panic(err)
	}
	autoMigration()
}

func autoMigration() {
	Db.AutoMigrate(&entity.Shop{})
	Db.AutoMigrate(&entity.Book{})
}

```
- DB接続情報が記載されています`congig`に記載された内容を`config.GetConfigs()`で取得して`gorm.Open(...)`で`mysql`に接続しています。
- 他に`autoMigration`で`model/entity`に記載されたDBのスキーマをマイグレーションしています。

## Router: server.go
```golang
func router() *gin.Engine {
	r := gin.Default()

	apiv1 := r.Group("/api/v1")
	{
		shopCtrl := controller.ShopController{}
		apiv1.GET("/shops", shopCtrl.IndexShop)
		apiv1.GET("/shops/:id", shopCtrl.ShowShop)
		apiv1.POST("/shops", shopCtrl.CreateShop)
		apiv1.PUT("/shops/:id", shopCtrl.UpdateShop)
		apiv1.DELETE("/shops/:id", shopCtrl.DeleteShop)

		bookCtrl := controller.BookController{}
		apiv1.GET("/books", bookCtrl.IndexBook)
		apiv1.GET("/books/:id", bookCtrl.ShowBook)
		apiv1.POST("/books", bookCtrl.CreateBook)
		apiv1.PUT("/books/:id", bookCtrl.UpdateBook)
		apiv1.DELETE("/books/:id", bookCtrl.DeleteBook)
	}
	return r
}
```
- ここではルーティングを記載しています
- `apiv1 := r.Group("/api/v1")`で`/api/v1`のパスを切っています
    - 他バージョンのapiを作ることになった場合は同様にして`apiv2 := r.Group("/api/v2")`と切ることで対応できます。
- `shopCtrl := controller.ShopController{}`で`controller`で定義した`ShopController`構造体をコントローラーとして指定できるようにしています。
    - したがって`apiv1.GET("/shops", shopCtrl.IndexShop)`として指定すれば`localhost/api/v1/shops`をGETでアクセスすれば`IndexShop`がコントローラーとして実行されます。
    - 次は`IndexShop`を見ていきましょう。

## Controller: controller/shop_controoler.go

```go
// IndexShop action: GET /shops
func (pc ShopController) IndexShop(c *gin.Context) {
	shops, err := model.GetShopAll()

	if err != nil {
		c.AbortWithStatus(404)
		fmt.Println(err)
	} else {
		view.RenderShops(c, shops)
	}
}

```
- ここは <font color="Red">***MVC***</font>の<font color="Red">***C***</font>すなわちコントローラーになります
- `shops, err := model.GetShopAll()`で<font color="Red">***Model***</font>から取得したSQLの結果を`view.RenderShops(c, shops)`で<font color="Red">***View***</font>に渡しています。
- 次は<font color="Red">***Model***</font>の`GetShopAll`と`entity`を一緒に見ましょう。

## Model: model/shop.go, model/entity/shop.go
```go
# 
// GetShopAll is get all Shop
func GetShopAll() ([]*entity.Shop, error) {
	db := db.GetDB()
	var u []*entity.Shop
	if err := db.Find(&u).Error; err != nil {
		return nil, err
	}

	return u, nil
}

# model/entity/shop.go
// Shop is shop models property
type Shop struct {
	ID              uint64 `gorm:"primary_key"`
	ShopName        string
	ShopDescription string
	CreatedAt       time.Time
	DeletedAt       *time.Time
}

```
- ここは <font color="Red">***MVC***</font>の<font color="Red">***M***</font>すなわちモデルになります。
- `model/entity/shop.go`で定義した構造体に`db.Find(&u)`の結果を当てはめている感じですね。
- また、ここはIndexなので`[]*entity.Shop`という形で`slice`(可変長配列のようなもの)で取るようにしています。
- ここで取得した値が`model`に返却されて`view.RenderShops(c, shops)`で`view`に渡されます。次は`RenderShops`が記載されている`view/shop.go`と一緒に`view/convert_function.go`と`view/schemas.go`を一緒に見ます。


## View: view/shop.go,view/convert_function.go,view/schemas.go

```go
# view/shop.go
type responseShops struct {
	Shops []shop `json:"shops"`
}

// RenderShops render shops.
func RenderShops(c *gin.Context, shops []*entity.Shop) {
	c.JSON(200, responseShops{Shops: convertToViewShops(shops)})
}

// RenderShop render shop.
func RenderShop(c *gin.Context, shop *entity.Shop) {
	c.JSON(200, convertToViewShop(shop))
}

# view/convert_function.go

func convertToViewShops(before []*entity.Shop) []shop {
	after := make([]shop, len(before))
	for i, p := range before {
		after[i] = convertToViewShop(p)
	}
	return after
}

func convertToViewShop(before *entity.Shop) shop {
	return shop{
		ID:              before.ID,
		ShopName:        before.ShopName,
		ShopDescription: before.ShopDescription,
	}
}

# view/schemas.go

type shop struct {
	ID              uint64 `json:"id"`
	ShopName        string `json:"shop_name"`
	ShopDescription string `json:"shop_description"`
}

```

- ここは <font color="Red">***MVC***</font>の<font color="Red">***V***</font>すなわちビューになります。
- 複雑ですが以下のような処理の流れになっています
    - `RenderShops`(statusコードを指定してjsonを返却)-> `responseShops`(indexなのでsliceを返す) -> `convertToViewShops`(モデルで取得した複数のレコードについて一つずつ`convertToViewShop`を呼び出す)->`convertToViewShop`(`shop`構造体に取得したレコードの値を当てはめる)->`shop`(実際に返却されるjsonの形式を構造体で定義しています)

- 以上のような流れで処理を行った結果以下のようなレスポンスが帰ってきます。

```bash
$ curl localhost:10080/api/v1/shops | jq
{
  "shops": [
    {
      "id": 1,
      "shop_name": "whisky shop",
      "shop_description": "Shop of liquors."
    },
    {
      "id": 2,
      "shop_name": "shusuky no mise",
      "shop_description": "Sake suki oyaji no mise."
    }
  ]
}

```

- 以上のようにして処理できますが`Rails`のインスタンスメソッドのようにモデルに生やしたメソッドの処理結果を`view`から呼び出してjsonに埋め込みたいという需要も有ると思います。
- 別APIで切った`books`の方でそのような対応を行っているので紹介したいと思います。

## Railsのインスタンスメソッドのような処理

```go
# model/entity/book.go
// Book is book models property
type Book struct {
	ID              uint64 `gorm:"primary_key"`
	BookName        string
	BookDescription string
	Sales           uint
	CreatedAt       time.Time
	DeletedAt       *time.Time
}

// Rank calculate rank of Book by sales.
func (book *Book) Rank() *string {

	if book.Sales > 10000 {
		rank := "ベストセラー"
		return &rank
	} else if book.Sales > 1000 {
		rank := "売れ筋"
		return &rank
	} else if book.Sales > 100 {
		rank := "そこそこ"
		return &rank
	} else if book.Sales > 10 {
		rank := "ちょいちょい"
		return &rank
	}
	return nil
}


```

- Railsのインスタンスメソッドのような処理を構造体にメソッドを生やすことで実現しました。
- ここの例では`model`に定義した`Book`に`Rank`を生やしています。
    - レコードのSalesの値に応じて数値またはnilを返す処理です。~~(あまりキレイな処理じゃないかもしれませんが。。。)~~
    - この`Rank`を`GET http://localhost:10080/api/v1/books/:id`で使用しています
    - `view`では以下のように単純に呼び出すことで使用しています。

```
# view/book.go
// RenderBook render book.
func RenderBook(c *gin.Context, book *entity.Book) {
	bookWithRank := bookWithRank{
		ID:              book.ID,
		BookName:        book.BookName,
		BookDescription: book.BookDescription,
		Sales:           book.Sales,
		Rank:            book.Rank(),
	}
	c.JSON(200, bookWithRank)
}

```
- `Rank:            book.Rank(),`の箇所でレコードの値のように単純に関数を呼び出して呼び出しています。
- そうすると以下のように関数の戻り値がjsonの`rank`キーに入って来ることが確認できます。
- こうしてインスタンス変数のような役割を実現することに成功しました。

```bash
$ curl localhost:10080/api/v1/books/1 | jq
{
  "id": 1,
  "book_name": "whisky diary",
  "book_description": "Secret diary of whisky!",
  "sales": 0,
  "rank": null
}

$ curl localhost:10080/api/v1/books/2 | jq
{
  "id": 2,
  "book_name": "shusuky diary",
  "book_description": "Secret diary of whisky!",
  "sales": 20,
  "rank": "ちょいちょい"
}

```

- ちなみに`curl localhost:10080/api/v1/books/1 | jq`の方では`null`が返ってきています。
- `golang`では`string型`にnullを指定することはできませんが以下のように構造体で`*string` と`Rank`に`string型のポインタ`を指定することで実現しています。
- ここら辺の`null`を返す対応については[他の記事](https://qiita.com/whisky-shusuky/items/a72627973beba36f90ca)でも書いたので良かったらご確認ください。

```go
type bookWithRank struct {
	ID              uint64  `json:"id"`
	BookName        string  `json:"book_name"`
	BookDescription string  `json:"book_description"`
	Sales           uint    `json:"sales"`
	Rank            *string `json:"rank"`
}
```

#感想・その他
- 記事では書いていませんがアプリ名を長くしたのは失敗でした
    - `import`する際に `import "gin-gorm-rails-like-sample-api/controller"`とパッケージ名が非常に長くなるからです。
    - 何か作るときは気をつけた方が良さそうです。。。
- `Rails`では[Rails チュートリアル](https://railstutorial.jp/)で手取り足取りMVCのアプリ作成を教えてもらうことができましたが、ginにはそれに当るものが中々見つからなかったため苦しみました。
    - この記事が私と同じようなことに苦しんでいる方の助けになれば幸いです。