Docker環境下でGoとRedisでAPIを実装する (Part.2)

# はじめに
本記事は、[GoとRedisでAPIを実装する (Part.1)](https://qiita.com/Morero/items/fb92879daee5f7055472)の続きである。まだ、読んでいない方は事前に読んでいただけると幸いである。

Part.2では、APIの実装から検証を行う。

まず今回の実装に至り、実行環境は以下の通りである。

# 1. 実行環境
* macOS Catalina Ver.10.15.2
* Docker version 19.03.5, build 633a0ea
* docker-compose version 1.24.1, build 4667896b
* golang 1.13.6
* Redis 5.0.7

## 1.1. ファイル構成

```terminal:file-tree
sample_project
├── docker
│   ├── api
│   │   └── Dockerfile-api //API用
│   └── database
│       └── Dockerfile-redis //DB用
├── docker-compose.yml
└── src
    └── app
        ├── controller
        │   ├── sender.go
        │   └── receiver.go
        ├── infrastructure
        │   └── database.go
        ├── interface
        │   └── user.go
        └── main.go
```

# 2. APIの実装

今回の実装では、JSON形式のデータを扱う事にする。

```json:sampleData.json
{
	// ユーザID
	"ID"	: "A000000",
	// ユーザ名
	"Name"	: "Bob",
	// 年齢
	"Age"	: 18,
}
```

Sender / Reciever それぞれの処理内容としては、以下の通りである。

* Sender側
    * JSON形式のデータをRedisに格納する。
        * Redisで使用するキーパターンは以下の通りである
        * 「ユーザID:ユーザ名」例)`A000000:Bob`
* Receiver側
    * キーを使用し、edis内からデータを取得する。

また、今回は重複データが存在する場合、データが更新されるようにする。

## 2.1. main.go
まず、`main.go`について説明する。ここで行う事はHTTP通信のみである。`gin.Default()`でデフォルトのミドルウェアと共にルータを作成し、それぞれのエンドポイントで行う処理を指定する。

```go:main.go
package main

import (
	"app/controller"
	"github.com/gin-gonic/gin"
)

func main() {
	router := gin.Default()

	// Sender
	router.POST("/sender", controller.Send())

	// Receiver
	router.GET("/receiver", controller.Receiver())

	// PORT環境変数が未指定の場合、デフォルトで8080で待受する
	router.Run()
}
```

今回は分かりやすい様、エンドポイントを`/sender`、`/receiver`にする。

## 2.2. interface/user.go
JSONで渡されるデータを受け取る為に、構造体を用意しておく。

```go:interface/user.go
package _interface

type UserInformation struct {
	// ユーザID
	ID		string	`json:"id"`
	// ユーザ名
	Name	string	`json:"name"`
	// 年齢
	Age		int 	`json:"age"`
}
```

## 2.3. Redisへの接続 / 切断
Redisへの接続処理を逐一記述するのは少し面倒であるし、複数箇所でこの処理を記述するのも可読性が悪い。その為、接続処理をインスタンス化する事で、処理を集約化した。

```golang:database.go
import (
	"github.com/garyburd/redigo/redis"
	"os"
)

type Redis struct {
	connection redis.Conn
}

// Redisへの接続
func NewRedis() *Redis {
	// IPポートの設定
	const ipPort = "redis:6379"
	// redisに接続する
	c, err := redis.Dial("tcp", ipPort)
	if err != nil {
		panic(err)
	}
	// 接続情報をConnインタフェースのconnectionに保存
	r := &Redis{
		connection: c,
	}
	return r
}
// Redisからの切断
func (r *Redis) CloseRedis() {
	// redisとの通信を切断する
	_ = r.connection.Close()
}
```

ポート番号はデフォルトの`6379`に設定している。
Connインタフェースは、Redisを使用する為の主要なインターフェースである。
また、切断処理もメソッド化しておく。

## 2.4. Sender
### 2.4.1. controller/sender.go
Sender側の処理について説明する。

```go:controller/sender.go
package controller

import (
	"app/infrastructure"
	"app/interface"
	"encoding/json"
	"fmt"
	"github.com/gin-gonic/gin"
	"net/http"
)

func Send() gin.HandlerFunc {
	return func(context *gin.Context) {
		// Redisに接続する
		redis := infrastructure.NewRedis()
		// Send()処理終了後にRedisとの接続を切断する
		defer redis.CloseRedis()

		// requestInformationをUserInformationで初期化する
		requestInformation := _interface.UserInformation{}

		// 構造体をBINDする
		err := context.Bind(&requestInformation)
		if err != nil{
			context.JSON(http.StatusBadRequest, gin.H{"Status": "BadRequest"})
		}

		// Redisで使用するキーの作成
		key := requestInformation.ID + ":" + requestInformation.Name

		// 作成した構造体requestInformationをJSONに変換する
		payload, err := json.Marshal(requestInformation)
		if err != nil {
			fmt.Println("JSON Marshal Error : ", err)
			return
		}

		// key, payloadを引数にRedisに追加する
		if err := redis.Set(key, payload); err != nil {
			fmt.Println("Failed to store data in Redis. ", err)
		} else {
			context.JSON(http.StatusOK, gin.H{"Status": "Successfully added to redis. "})
		}
	}
}
```
一度、取得したデータを構造体`requestInformation`に格納し、その後に`json.Marshal()`でJSON文字列に変換する。今回、Redisで使用するキーの生成はこのファイル内で簡素的に生成している。生成した`key`と`payload`を引数に`database.go`の関数`Set(key, payload)`に渡す。

### 2.4.2. infrastructure/database.go (SET)
先程の、`database.go`にRedisへデータを追加する処理を追記する。

```golang:database.go
import (
	"fmt"
	"github.com/garyburd/redigo/redis"
	"os"
)

// ...省略

// Redisへのデータ追加
func (r *Redis) Set(key string, payload []byte) error {
	// 生成したキーが既に存在するかチェックする
	if r.keyExist(key) == true {
		fmt.Println("Delete the key because it was already registered in redis.")
		fmt.Println("Update an existing key.")
		// 存在する場合、データを更新する
		r.update(key, payload)
	} else {
		// キーをRedisに追加する
		if _, err := r.connection.Do("SET", key, payload); err != nil {
			fmt.Println("infrastructure/database/Set() : ", err)
			os.Exit(1)
			return err
		}
	}
	return nil
}

// キーチェック
func (r *Redis) keyExist(key string) bool {
	// キーが既にRedis内に存在するかチェックする
	result, err := redis.Bool(r.connection.Do("EXISTS", key))
	if err != nil {
		fmt.Println("infrastructure/database/keyExist() : ", err)
	}
	return result
}

// データの更新
func (r *Redis) update(key string, payload []byte) {
	// キーから値を取得後、新たなデータを登録する
	_, err := r.connection.Do("GETSET", key, payload)
	if err != nil {
		fmt.Println("infrastructure/database/update() : ", err)
	}
}
```
今回は、重複したデータを削除する処理を入れているので、一度`keyExist()`でRedis内にキーが存在するかチェックする。存在する場合は、`update()`で既存のキーからデータを取得し、新たなデータを更新する。redisには`UPDATE`はないので、代わりに`GETSET`を使用し、更新を行う。

これでSender側の処理は終了である。

## 2.5 Receiver
### 2.5.1. controller/receiver.go
次にReceiver側の処理について説明する。ここでは、パラメータから取得した`key`を使用し、redisからデータを取得する。

```golang:controller/receiver.go
package controller

import (
	"app/infrastructure"
	_interface "app/interface"
	"encoding/json"
	"fmt"
	"github.com/gin-gonic/gin"
	"net/http"
)

func Receive() gin.HandlerFunc {
	return func(context *gin.Context) {
		// Redisに接続
		redis := infrastructure.NewRedis()
		// Receive()処理終了後にRedisとの接続を切断する
		defer redis.CloseRedis()

		// クエリストリングパラメーターを取得
		key := context.Query("key")

		// responseInformationをUserInformationで初期化する
		responseInformation := _interface.UserInformation{}

		// Redisからデータを取得する
		if payload, err := redis.Get(key); err != nil {
			fmt.Println("Failed to get data from Redis. :", err)
		} else {
			// Redisから取得したpayloadをGo Object(構造体)に変換する
			if err := json.Unmarshal(payload, &responseInformation); err != nil {
				fmt.Println("Could not Unmarshal the retrieved json. :", err)
			}
			context.JSON(http.StatusOK, responseInformation)
		}
	}
}
```
Receiverでは、Redisに格納されているデータをクエリストリングパラメータ(URLパラメータとも言う)から、データを取得する。取得後は、`json.Unmarshal()`を行い、`Go Object`として構造体`UserInformation`に変換する。

### 2.5.2. infrastructure/database.go (GET)
また先程の、`database.go`にGET処理を追記する。

```golang:database.go
import (
	"fmt"
	"github.com/garyburd/redigo/redis"
	"os"
)

// ...省略

func (r *Redis) Get(key string) ([]byte, error) {
	// キーを使用し、Redisからデータを追加する
	payload, err := redis.Bytes(r.connection.Do("GET", key))
	if err != nil {
		fmt.Println("infrastructure/database/Set() : ", err)
		return payload, err
	}
	return payload, err
}
```

以上で、Sender / Receiverの処理は終了である。

では、最後に検証を行う。

# 3. 検証
検証では、[Talend API Tester](https://chrome.google.com/webstore/detail/talend-api-tester-free-ed/aejoelaoggembcahagimdiliamlcdmfm?hl=ja)や、[Postman](https://www.getpostman.com/)と言ったツールを使用すると良い。今回は、Postmanを使用して検証を行う。

## 3.1. Sender

<img width="1361" alt="スクリーンショット 2020-02-01 17.02.03.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/223371/13b4246a-ead8-9bf0-7ea5-80825ea5a88c.png">

以下のURLにJSONをPOST通信で渡している。正常に渡せていれば結果として`"Status": "Successfully added to redis. "`が返ってくる。

* URL
    * `http://localhost:8080/sample_project/sender`

* 結果

```json:Status-Success
{
    "Status": "Successfully added to redis. "
}
```

また、Redisに正しくデータが登録されているかどうかを確認する。確認は` sample_project/redis:0.1.0 `のコンテナに入り、以下のコマンドを確認すると良い。

```terminal:redis-cli
$ docker exec -it [CONTAINER ID] sh
/data # redis-cli
127.0.0.1:6379> GET key(自分で設定したキー)
"{\"id\":\"sample001\",\"name\":\"test001\",\"age\":99}"
```

IDEなどでDBが確認できるプラグインもあるが、その際は`localhost:6379`を指定して確認すると良い。

## 3.2. Receiver

<img width="1361" alt="スクリーンショット 2020-02-01 17.18.26.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/223371/fced5f29-4b73-31d9-787a-4e6f24437f35.png">

GET通信なので、URL内にパラメータとして`key`を渡す。データが取得できれば、結果として登録したデータが返ってくる。

検証としては、以上である。

# 4. まとめ
今回は、2つの記事に渡り、APIの実装について記述した。今回の内容を参考にしていただければ幸いである。

また、この実装は簡素的なものであるため、懸念点なども多い。今後は、その点の改善を行っていこうと思う。

# 追記
* [サンプルコード](https://github.com/t4i5uKE/go-api)
    * ホットリロードをFreshから、[Air](https://github.com/cosmtrek/air)に変更