はじめに
Go(Echo)を用いたAPIサーバを構築したい方向けとなっております。
前回:Docker上でGo(Echo)+MySQLのAPIサーバを構築する(1)GoとMySQLを接続し、テーブルを作成する
この記事では、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を作成します。
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を用いて簡単なサーバを構築します。
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
# バージョン指定
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
# 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を行う関数を定義します。
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
を追加し、以下のように記述します。
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
のルーティングを新たに作成する際に指定します。
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メソッドを送信してレコードを増やしてみます。
レコードの追加が成功し、レスポンスとして追加したレコードの情報が返ってきました(^▽^)/
GET
次に、GETメソッドを使用してUserテーブルのレコードを取得したいと思います。
こちらも正常に動作が完了し、レコードを取得できていますね。👀
(筆者が何度もPOSTしたせいで、先ほど追加したレコードが6番目になっているのはお気になさらず)
PUT
先ほど追加したレコードの情報を更新してみましょう。
更新が成功し、レスポンスとして更新したレコードの情報が返ってくることを確認できました。
DELETE
最後に、更新したレコードを削除してみましょう。
こちらも、成功時に削除したレコードの情報が返ってくることを確認できました。
これで、GoからMySQLのDBにCRUD操作を行うAPIサーバを構築することができました!
さいごに
最後までご覧いただきありがとうございます。
今回は、CRUD操作を行うAPIサーバをGoで構築する所まで行いました。
気が向いたら(需要があるかは知りませんが)今回のAPIサーバとNext.jsを連携して、フロントエンドからAPIを叩いてCRUD操作を行うまでを記事に書きたいと思います。