Go(Echo)+MySQLでCRUDを行えるAPIサーバをDocker上で構築する（2）APIサーバを構築する

# はじめに
Go（Echo）を用いたAPIサーバを構築したい方向けとなっております。
**前回：[Docker上でGo(Echo)+MySQLのAPIサーバを構築する（１）GoとMySQLを接続し、テーブルを作成する](https://qiita.com/Senritsu420/items/9a07c59a991d8e5042c4)**
この記事では、Go（Echo)を用いてAPIサーバを構築するまで説明します。
間違いや改善点などあればお気軽に仰っていただけると幸いです。

環境

Docker：v24.0.6
Docker Compose：v2.4.1
# 最終的な構成図
```
.
├── docker-compose.yml
├── golang
│   ├── Dockerfile
│   └── app
│       ├── handler
│       │   └── handler.go
│       ├── go.mod
│       ├── go.sum
│       ├── main.go
│       └── database
│           └── database.go
└── mysql
    └── Dockerfile
```

# 前回のおさらい
前回は、Docker Composeを用いてGoとMySQLを接続し、Userテーブルを作成する所まで行いました。
以下が、前回時点でのディレクトリ構成です。
```
.
├── docker-compose.yml
├── golang
│   ├── Dockerfile
│   └── app
│       ├── go.mod
│       ├── go.sum
│       └── main.go
└── mysql
    ├── Dockerfile
    └── .env
```

# 1. Echoを用いてサーバを起動する
前回main.goにDBの操作を記述していたのですが、ファイルを分けた方が可読性が向上するため新しくapp直下にdatabase/database.goを作成します。
``` database/database.go
package database

import (
	"errors"
	"fmt"
	"os"

	"gorm.io/driver/mysql"
	"gorm.io/gorm"
)

// MySQLに作成するUserテーブルの定義
type User struct {
	// gorm.Modelをつけると、idとCreatedAtとUpdatedAtとDeletedAtが作られる
	gorm.Model

	Name string
	Age  int
}

// 外部参照可能なDB変数を定義
var Db *gorm.DB

// dbInit()が呼び出されたときに最初に処理される関数
func init() {
	Db = dbInit()
	// Userテーブル作成
	Db.AutoMigrate(&User{})
}

// DBを起動させる
func dbInit() *gorm.DB {
	// [ユーザ名]:[パスワード]@tcp([ホスト名]:[ポート番号])/[データベース名]?charset=[文字コード]
	dsn := fmt.Sprintf(`%s:%s@tcp(db:3306)/%s?charset=utf8mb4&parseTime=True`, os.Getenv("MYSQL_USER"), os.Getenv("MYSQL_PASSWORD"), os.Getenv("MYSQL_DATABASE"))
	// DBへの接続を行う
	db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})

	// エラーが発生した場合、エラー内容を表示
	if err != nil {
		fmt.Println(err)
	}
	// 接続に成功した場合、「db connected!!」と表示する
	fmt.Println("db connected!!")
	return db
}
```
次に、`main.go`を編集します。`database.go`に移行した部分は削除し、Echoを用いて簡単なサーバを構築します。
``` main.go
package main

import (
    "net/http"

	"github.com/labstack/echo"
	"github.com/labstack/echo/middleware"
)

func main() {
    // Echoインスタンスを作成
    e := echo.New()

    // httpリクエストの情報をログに表示
	e.Use(middleware.Logger())
	// パニックを回復し、スタックトレースを表示
	e.Use(middleware.Recover())

    // ルートを設定（第一引数にエンドポイント、第二引数にハンドラーを指定）
    e.GET("/", hello)

    // サーバーをポート番号8080で起動
    e.Logger.Fatal(e.Start(":8080"))
}

// ハンドラーを定義（どういう処理を実行するか）
func hello(c echo.Context) error {
  return c.String(http.StatusOK, "Hello, World!")
}
```
この例では、`http://localhost:8080`にアクセスすると「Hello World!」という文字列が返ってきます。

# 2. ホットリロードライブラリ「Air」を導入する
現状ですと、ソースコードを変更する度にビルドする必要があるので、GoのホットリロードライブラリであるAirを導入していきます。
まず、golang/DockerfileでAirをインストールし、実行するように変更します。
### golang/Dockerfile
``` Dockerfile
# バージョン指定
FROM golang:1.19.2-alpine3.16
# go moduleモードに設定
ENV GO111MODULE on
# 必要なものをインストール
RUN apk update && \
    apk --no-cache add git && \
    apk add bash
# 作業ディレクトリ指定
WORKDIR /go/src/app
# go.mod作成
RUN go mod init app
# go.mod更新&Airインストール
RUN go mod tidy && \
    go install github.com/cosmtrek/air@v1.40.4
# Air起動
CMD ["air", "-c", ".air.toml"]
```

次に、main.goと同じ場所（今回はgolang/app）にAirの設定ファイルである`.air.toml`を追加します。
`.air.toml`の中身は、公式のテンプレートを使用しています。
[https://github.com/cosmtrek/air/blob/master/air_example.toml](https://github.com/cosmtrek/air/blob/master/air_example.toml)

``` .air.toml
# Config file for [Air](https://github.com/cosmtrek/air) in TOML format

# Working directory
# . or absolute path, please note that the directories following must be under root.
root = "."
tmp_dir = "tmp"

[build]
# Array of commands to run before each build
pre_cmd = ["echo 'hello air' > pre_cmd.txt"]
# Just plain old shell command. You could use `make` as well.
cmd = "go build -o ./tmp/main ."
# Array of commands to run after ^C
post_cmd = ["echo 'hello air' > post_cmd.txt"]
# Binary file yields from `cmd`.
bin = "tmp/main"
# Customize binary, can setup environment variables when run your app.
full_bin = "APP_ENV=dev APP_USER=air ./tmp/main"
# Watch these filename extensions.
include_ext = ["go", "tpl", "tmpl", "html"]
# Ignore these filename extensions or directories.
exclude_dir = ["assets", "tmp", "vendor", "frontend/node_modules"]
# Watch these directories if you specified.
include_dir = []
# Watch these files.
include_file = []
# Exclude files.
exclude_file = []
# Exclude specific regular expressions.
exclude_regex = ["_test\\.go"]
# Exclude unchanged files.
exclude_unchanged = true
# Follow symlink for directories
follow_symlink = true
# This log file places in your tmp_dir.
log = "air.log"
# Poll files for changes instead of using fsnotify.
poll = false
# Poll interval (defaults to the minimum interval of 500ms).
poll_interval = 500 # ms
# It's not necessary to trigger build each time file changes if it's too frequent.
delay = 0 # ms
# Stop running old binary when build errors occur.
stop_on_error = true
# Send Interrupt signal before killing process (windows does not support this feature)
send_interrupt = false
# Delay after sending Interrupt signal
kill_delay = 500 # nanosecond
# Rerun binary or not
rerun = false
# Delay after each executions
rerun_delay = 500
# Add additional arguments when running binary (bin/full_bin). Will run './tmp/main hello world'.
args_bin = ["hello", "world"]

[log]
# Show log time
time = false
# Only show main log (silences watcher, build, runner)
main_only = false

[color]
# Customize each part's color. If no color found, use the raw app log.
main = "magenta"
watcher = "cyan"
build = "yellow"
runner = "green"

[misc]
# Delete tmp directory on exit
clean_on_exit = true

[screen]
clear_on_rebuild = true
keep_scroll = true
```

これで、Dockerコンテナを再起動しなくてもソースコードの変更が反映されるようになります。

# 3. GoのORMであるgormを用いてDBのCRUD操作が行えるようにする
次は、先ほど作成した`database.go`に追記して、DBに対してGET、POST、PUT、DELETEを行う関数を定義します。
``` database.go
package database

import (
	"errors"
	"fmt"
	"os"

	"gorm.io/driver/mysql"
	"gorm.io/gorm"
)

// MySQLに作成するUserテーブルの定義
type User struct {
	// gorm.Modelをつけると、idとCreatedAtとUpdatedAtとDeletedAtが作られる
	gorm.Model

	Name string
	Age  int
}

// 外部参照可能なDB変数を定義
var Db *gorm.DB

// dbInit()が呼び出されたときに最初に処理される関数
func init() {
	Db = dbInit()
	// Userテーブル作成
	Db.AutoMigrate(&User{})
}

// DBを起動させる
func dbInit() *gorm.DB {
	// [ユーザ名]:[パスワード]@tcp([ホスト名]:[ポート番号])/[データベース名]?charset=[文字コード]
	dsn := fmt.Sprintf(`%s:%s@tcp(db:3306)/%s?charset=utf8mb4&parseTime=True`,
            os.Getenv("MYSQL_USER"), os.Getenv("MYSQL_PASSWORD"), os.Getenv("MYSQL_DATABASE"))
	// DBへの接続を行う
	db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})

	// エラーが発生した場合、エラー内容を表示
	if err != nil {
		fmt.Println(err)
	}
	// 接続に成功した場合、「db connected!!」と表示する
	fmt.Println("db connected!!")
	return db
}

// ここから追加

// レコード全件取得
func ReadAll(db *gorm.DB) ([]User, error) {
	// user構造体のスライスを作成
	users := []User{}
	// 全てのuser情報を取得
	result := db.Find(&users)
	// エラー発生時はエラー内容を表示
	if result.Error != nil {
		return users, result.Error
	}
	// 全てのuser情報を返す
	return users, result.Error
}

// レコード単体取得
func ReadOne(db *gorm.DB, id int) (User, error) {
	// user構造体を作成
	user := User{}
	// 特定のidのuser情報を取得
	result := db.First(&user, id)
	// idが見つからない場合はエラー内容を表示
	if errors.Is(result.Error, gorm.ErrRecordNotFound) {
		return user, result.Error
	}
	// 特定のuser情報を返す
	return user, result.Error
}

// レコード作成
func InsertUser(db *gorm.DB, name string, age int) error {
	// 引数を元に追加するuser構造体を作成
	user := User{
		Name: name,
		Age:  age,
	}
	// 新規userを作成する
	result := db.Create(&user)
	// エラー発生時はエラー内容を表示
	if result.Error != nil {
		return result.Error
	}

	return nil
}

// レコード更新
func UpdateUser(db *gorm.DB, id int, name string, age int) error {
	// 更新するユーザを取得する
	user, err := ReadOne(db, id)
	if err != nil {
		return err
	}
	// 構造体にidがある場合はupdateされる
	user.Name = name
	user.Age = age
	result := db.Save(&user)
	// エラー発生時はエラー内容を表示
	if result.Error != nil {
		return result.Error
	}

	return nil
}

// レコード削除
func DeleteUser(db *gorm.DB, id int) (User, error) {
	// 削除するuser情報を取得
	user, err := ReadOne(db, id)
	if err != nil {
		return user, err
	}
	// DeletedAtがある場合は論理削除になる
	db.Where("id = ?", id).Delete(&User{})
	// 物理削除の場合は以下のようになる
	// db.Unscoped().Where("id = ?", id).Delete(&User{})

	// 削除したuser情報を返す
	return user, err
}
```
こちらを元にハンドラーを作成し、エンドポイントにアクセスした際にこの関数からCRUD操作を行う想定です。

# 4. ハンドラーとルーティングを作成し、GoからCRUD操作を行う
エンドポイントにアクセスした際に呼び出されるハンドラーを定義します。
appディレクトリ直下に`handler/handler.go`を追加し、以下のように記述します。
``` handler/handler.go
package handler

import (
	"app/database"
	"net/http"
	"strconv"

	"github.com/labstack/echo"
)

// 全てのuser情報を取得し、JSON形式で返す
func GetAllUser(c echo.Context) error {
	// 全てのuser情報を取得
	users, err := database.ReadAll(database.Db)
	if err != nil {
		return err
	}
	// 全てのuser情報をJSON形式で返す
	return c.JSON(http.StatusOK, users)
}

// 特定のuser情報を取得し、JSON形式で返す
func GetOneUser(c echo.Context) error {
	// パスパラメータ「id」を取得
	param := c.Param("id")
	// idを整数型に変換
	id, _ := strconv.Atoi(param)
	// 特定のuser情報を取得
	user, err := database.ReadOne(database.Db, id)
	if err != nil {
		return err
	}
	// 特定のuser情報をJSON形式で返す
	return c.JSON(http.StatusOK, user)
}

// 送られてきた情報を元に新規userを作成し、JSON形式で返す
func PostNewUser(c echo.Context) error {
	// user構造体作成
	u := new(database.User)
	// 送られてきた情報を元に構造体へのデータ格納を行う
	err := c.Bind(u)
	// エラー発生時はエラーを返す
	if err != nil {
		return err
	}
	// 構造体の情報を元に新規userを作成
	database.InsertUser(database.Db, u.Name, u.Age)
	// 新規user情報をJSON形式で返す
	return c.JSON(http.StatusOK, u)
}

// 送られてきた情報を元にuser情報を更新し、JSON形式で返す
func PutUser(c echo.Context) error {
	// パスパラメータ「id」を取得
	param := c.Param("id")
	// idを整数型に変換
	id, _ := strconv.Atoi(param)
	// user構造体作成
	u := new(database.User)
	// 送られてきた情報を元に構造体へのデータ格納を行う
	err := c.Bind(u)
	// エラー発生時はエラーを返す
	if err != nil {
		return err
	}
	// 構造体の情報を元にuserを更新
	database.UpdateUser(database.Db, id, u.Name, u.Age)
	// 更新したuser情報をJSON形式で返す
	return c.JSON(http.StatusOK, u)
}

// 送られてきた情報を元にuserを削除し、JSON形式で返す
func DeleteUser(c echo.Context) error {
	// パスパラメータ「id」を取得
	param := c.Param("id")
	// idを整数型に変換
	id, _ := strconv.Atoi(param)
	// 構造体の情報を元にuserを更新
	user, err := database.DeleteUser(database.Db, id)
	if err != nil {
		return err
	}
	// 更新したuser情報をJSON形式で返す
	return c.JSON(http.StatusOK, user)
}
```
このハンドラーを、`main.go`のルーティングを新たに作成する際に指定します。
``` main.go
package main

import (
	"app/handler"

	"github.com/labstack/echo"
	"github.com/labstack/echo/middleware"
)

func main() {
	// Echoインスタンスを作成
	e := echo.New()

	// ミドルウェアを設定
	e.Use(middleware.Logger())
	e.Use(middleware.Recover())
 
    /* ここから追加 */
	// ルートを設定（第一引数にエンドポイント、第二引数にハンドラーを指定）
	e.GET("/users", handler.GetAllUser)
	e.POST("/users", handler.PostNewUser)
	e.GET("/users/:id", handler.GetOneUser)
	e.PUT("/users/:id", handler.PutUser)
	e.DELETE("/users/:id", handler.DeleteUser)
    /* ここまで */

	// サーバーをポート番号8080で起動
	e.Logger.Fatal(e.Start(":8080"))
}

```
# 4. 動作確認

これで記述が完了したので、`docker compose up`を実行して動作確認してみます。
動作確認には、Postmanを使用しています。
### POST
まずは、テーブルに何も入っていないので`http://localhost:8080/users`にPOSTメソッドを送信してレコードを増やしてみます。
![postman post user.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2653241/219d1e14-5d46-28f1-5a1e-83f13d1dffbb.png)
レコードの追加が成功し、レスポンスとして追加したレコードの情報が返ってきました(^▽^)/

### GET
次に、GETメソッドを使用してUserテーブルのレコードを取得したいと思います。
![postman get user.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2653241/030b9cdf-8399-e962-e184-279fabf9d460.png)
こちらも正常に動作が完了し、レコードを取得できていますね。👀
（筆者が何度もPOSTしたせいで、先ほど追加したレコードが6番目になっているのはお気になさらず）

### PUT
先ほど追加したレコードの情報を更新してみましょう。
![postman put user.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2653241/7038e4eb-c9cb-c7ea-9e41-ea219eeb28ab.png)
更新が成功し、レスポンスとして更新したレコードの情報が返ってくることを確認できました。

### DELETE
最後に、更新したレコードを削除してみましょう。
![postman delete user.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2653241/bc2a757c-f542-6a50-e0c0-2b08ab93ed24.png)
こちらも、成功時に削除したレコードの情報が返ってくることを確認できました。

これで、GoからMySQLのDBにCRUD操作を行うAPIサーバを構築することができました！

# さいごに
最後までご覧いただきありがとうございます。
今回は、CRUD操作を行うAPIサーバをGoで構築する所まで行いました。
気が向いたら（需要があるかは知りませんが）今回のAPIサーバとNext.jsを連携して、フロントエンドからAPIを叩いてCRUD操作を行うまでを記事に書きたいと思います。