次に試しにmain.goのファイルを以下のように修正し、ファイルを保存します。
package main
import (
// Ginをインポート
"github.com/gin-gonic/gin"
)
funcmain() {
router := gin.Default()
// JSON形式で「"message" : "Hello World" 」を出力するAPI
router.GET("/" , func(c *gin.Context) {
c.JSON(200, gin.H{
"message" : "Hello World !!" ,
})
})
router.Run(":3001" )
}
もう一度ブラウザを更新して「localhost」にアクセスし、ファイルの修正が反映されればOKです。
尚、gitを導入している場合は、「.gitignore」を以下のようにしておきます。
/.env
/nginx/log/*
!/nginx/log/.keep
/tmp/db/*
!/tmp/db/.keep
/src/tmp
その他、Air導入後はコンテナの停止や再起動について、「docker compose stop」や「docker compose start」が上手く機能しなくなる ため、「docker compose down」や「docker compose up -d」を使う ようにして下さい。
routerとcontrollerを追加
次にRailsのようなディレクトリ構成を参考にしたいので、routerとcontrollerを追加し、それらにAPIの設定を集約できるようにします。
まずは以下のコマンドを実行し、routerとcontrollerを追加します。
$ cd src
$ mkdir router
$ mkdir controller
$ cd router
$ touch router.go
$ cd ..
$ cd controller
$ touch index_controller.go
$ cd .. /..
次に「index_controller.go」と「router.go」の中身を次のようにします。
package controller
import (
"github.com/gin-gonic/gin"
)
func Index(c *gin.Context) {
c.JSON(200, gin.H{
"message" : "Hello World Index !!" ,
})
}
package router
import (
"github.com/gin-gonic/gin"
"go_sample/controller"
)
func Init() {
// routerの初期化
router := gin.Default()
// routerの設定
router.GET("/" , controller.Index)
// routerを起動
router.Run(":3001" )
}
次に「main.go」を次のように修正します。
package main
import (
"go_sample/router"
)
func main() {
// routerの初期化
router.Init()
}
次に以下のコマンドを実行し、goのパッケージ管理ファイルを更新します。
$ docker compose exec api go mod tidy
次にファイルの追加等をしたのでコンテナを再起動させます。
$ docker compose down
$ docker compose build --no-cache
$ docker compose up -d
次にブラウザから「localhost」にアクセスし、以下のようにメッセージが出力されればOKです。
config設定を追加
次に環境変数に設定した値など、各種設定値をまとめて取得できるようにするため、config設定を追加します。
まずは以下のコマンドを実行し、各種ディレクトリやファイルを追加します。
$ cd src
$ mkdir config
$ cd config
$ touch config.go
$ touch config_dev.yml
※今回の例では、config関連の設定をするのにviperというパッケージを利用します。そのため、環境ごとの設定値を記載する「config_dev.yml」というymlファイルを作成しています。
次に最初に事前に追加しておいた「.env」ファイルに、この後に使うDB(MySQL)用の設定値などを以下のように設定しておきます。
ENV=dev
TZ=Asia/Tokyo
MYSQL_DATABASE=go_sample_db
MYSQL_USER=go_sample_user
MYSQL_PASSWORD=go_sample_password
MYSQL_ROOT_PASSWORD=go_sample_root_password
次に上記で作成した環境別(開発環境用ならdev、本番環境用ならprodなどのようにファイルを分けることが可能)の設定ファイル「config_dev.yml」を以下のように設定しておきます。
※このファイルにはパスワードなどのセキュリティに関わらない項目に関して設定値を記載します。セキュリティに関わるものは「.env」などを利用した環境変数に設定して下さい。
db:
type: "mysql"
host: "db"
port: 3306
charset: "utf8mb4"
parseTime: true
loc: "Local"
migrate:
filePath: "file://database/migrate"
次に上記で作成したconfig用のパッケージ「config.go」を以下のように修正します。
※外部からこのパッケージを利用するため、変数名などの1文字目は大文字にする必要があるのでご注意下さい。(例えばtype Config structだったり、Envだったり)
package config
import (
"fmt"
"os"
"github.com/spf13/viper"
"github.com/fsnotify/fsnotify"
)
// config設定の構造体
type Config struct {
Env string
Tz string
Db Db `yml:db`
Migrate Migrate `yml:migrate`
}
type Db struct {
Type string `yml:type`
Host string `yml:host`
Port int `yml:port`
Charset string `yml:charset`
ParseTime bool `yml:parseTime`
Loc string `yml:loc`
Database string
User string
Password string
}
type Migrate struct {
FilePath string `yml:filePath`
}
var cfg *Config
func Init() {
// viperの初期設定
viper.SetConfigName("config_" + fmt.Sprintf("%s" , os.Getenv("ENV" )))
viper.SetConfigType("yml" )
viper.AddConfigPath("config/" )
// configファイル更新時に再読み込み
viper.WatchConfig()
viper.OnConfigChange(func(e fsnotify.Event) {
fmt.Println("Config file changed:" , e.Name)
viper.Unmarshal(&cfg)
})
// configファイルの読み込み
err := viper.ReadInConfig()
if err != nil {
panic(err)
}
// 読み込んだデータを変数cfgに設定
err = viper.Unmarshal(&cfg)
if err != nil {
panic(err)
}
// 環境変数の値を変数cfgに設定
cfg.Env = os.Getenv("ENV" )
cfg.Tz = os.Getenv("TZ" )
cfg.Db.Database = os.Getenv("MYSQL_DATABASE" )
cfg.Db.User = os.Getenv("MYSQL_USER" )
cfg.Db.Password = os.Getenv("MYSQL_PASSWORD" )
}
func GetConfig() *Config {
return cfg
}
次にmain.goを以下のように修正して上記で作成したconfigファイルの初期化を実行し、「config.GetConfig()」で各種設定値を取得して利用できるようにします。
package main
import (
"go_sample/config"
"go_sample/router"
)
func main() {
// configの初期化
config.Init()
// routerの初期化
router.Init()
}
これでconfig設定の追加が完了したので、試しにconfig設定で取得した値を画面に表示するAPIを作成してみます。
まずは以下のコマンドを実行し、config用のコントローラーを作成します。
$ cd src/controller
$ touch config_controller.go
そして、config_controller.goについては以下のように修正します。
package controller
import (
"github.com/gin-gonic/gin"
"go_sample/config"
)
func ConfigIndex(c *gin.Context) {
// config設定を取得
cfg := config.GetConfig()
c.JSON(200, gin.H{
"ENV" : cfg.Env,
"Tz" : cfg.Tz,
"DB.Type" : cfg.Db.Type,
"DB.Host" : cfg.Db.Host,
"DB.Port" : cfg.Db.Port,
"DB.Charset" : cfg.Db.Charset,
"DB.ParseTime" : cfg.Db.ParseTime,
"DB.Loc" : cfg.Db.Loc,
"DB.Database" : cfg.Db.Database,
"DB.User" : cfg.Db.User,
})
}
次にrouter.goを以下のように修正します。
package router
import (
"github.com/gin-gonic/gin"
"go_sample/controller"
)
func Init() {
// routerの初期化
router := gin.Default()
// routerの設定
router.GET("/" , controller.Index)
// APIの設定
apiV1 := router.Group("/api/v1" )
apiV1.GET("/config" , controller.ConfigIndex)
// routerを起動
router.Run(":3001" )
}
これで準備が整ったので、以下のコマンドを実行し、modファイルの修正およびコンテナの再起動を行います。
$ docker compose exec api go mod tidy
$ docker compose down
$ docker compose build --no-cache
$ docker compsoe up -d
次にブラウザから「localhost/api/v1/config」にアクセスし、以下のようにconfig設定から取得した値が表示されればOKです。
database(MySQL)とマイグレーション(usersテーブル)の追加
次にdatabase(MySQL)に関する部分を追加するため、以下のコマンドを実行してディレクトリやファイルを作成します。
$ cd src
$ mkdir database
$ cd database
$ touch database.go
$ mkdir migrate
$ cd .. /..
そして、作成した「database.go」の中身は以下のように修正します。
package database
import (
"fmt"
"log"
"go_sample/config"
"gorm.io/driver/mysql"
"gorm.io/gorm"
"github.com/golang-migrate/migrate/v4"
_ "github.com/golang-migrate/migrate/v4/database/mysql"
_ "github.com/golang-migrate/migrate/v4/source/file"
)
var db *gorm.DB
var m *migrate.Migrate
func Init() {
// config設定を取得
cfg := config.GetConfig()
// DBの接続先設定
dsn := fmt.Sprintf(
"%s:%s@tcp(%s:%d)/%s" ,
cfg.Db.User,
cfg.Db.Password,
cfg.Db.Host,
cfg.Db.Port,
cfg.Db.Database,
)
dsn_option := fmt.Sprintf(
"?charset=%s&parseTime=%t&loc=%s" ,
cfg.Db.Charset,
cfg.Db.ParseTime,
cfg.Db.Loc,
)
dsn_mysql := dsn + dsn_option
// DBに接続
var err error
db, err = gorm.Open(mysql.Open(dsn_mysql), &gorm.Config{})
if err != nil {
panic(err)
}
// マイグレーション設定
dsn_m := fmt.Sprintf(
"%s://%s" ,
cfg.Db.Type,
dsn,
)
m, err = migrate.New(
cfg.Migrate.FilePath,
dsn_m,
)
if err != nil {
panic(err)
}
// マイグレーションの実行
err = m.Up()
// エラーメッセージが「no change」の場合もスキップ
if err != nil && err.Error() != "no change" {
log.Printf("m.Up() Error Message: %s\n" , err)
}
}
func GetDB() *gorm.DB {
return db
}
func GetM() *migrate.Migrate {
return m
}
func Close() {
getDB, err := db.DB()
if err != nil {
panic(err)
}
getDB.Close()
}
次にコンテナを起動中の場合は、以下のコマンドを実行して停止させておきます。
次に「docker/dev/Dockerfile」、「docker-compose.yml」、「main.go」を以下のように修正します。
FROM golang:1.20.2-alpine
RUN go install github.com/cosmtrek/air@latest
RUN go install -tags mysql github.com/golang-migrate/migrate/v4/cmd/migrate@latest
RUN apk update && \
apk upgrade && \
# パッケージのインストール(--no-cacheでキャッシュ削除)
apk add --no-cache \
git \
gcc \
musl-dev
WORKDIR /go/src
※マイグレーション用として、今回は「golang-migrate」を使用します。
version: '3'
services:
# APIに関する設定
api:
container_name: go_sample_api
build:
context: .
dockerfile: ./src/docker/dev/Dockerfile
env_file:
- ./.env
command: air -c .air.toml
volumes:
- ./src:/go/src
tty: true
stdin_open: true
ports:
- "3001:3001"
depends_on:
- db
# Webサーバーに関する設定
web:
container_name: go_sample_web
build:
context: .
dockerfile: ./nginx/Dockerfile
volumes:
- ./nginx/log:/var/log/nginx
ports:
- "80:80"
depends_on:
- api
# DBに関する設定
db:
container_name: go_sample_db
image: mysql:8.0.32
env_file:
- ./.env
ports:
- 3306:3306
volumes:
- ./tmp/db:/var/lib/mysql
※DBには、今回はMySQLを使用します。
package main
import (
"go_sample/config"
"go_sample/database"
"go_sample/router"
)
func main() {
// configの初期化
config.Init()
// DBの初期化
database.Init()
defer database.Close()
// routerの初期化
router.Init()
}
次に以下のコマンドを実行し、コンテナを再ビルドします。
$ docker compose build --no-cache
次に以下のコマンドを実行し、一時的にapiコンテナを起動させてgo.modファイルの修正および、マイグレーション用のファイルを作成します。
$ docker compose run --rm api go mod tidy
$ docker compose run --rm api migrate create -ext sql -dir database/migrate -seq create_users_table
※今回の例では、ユーザー情報用のusersテーブルのマイグレーションファイルを作成します。
次に上記のmigrateコマンドでディレクトリ「src/database/migrate」配下にファイル「000001_create_users_table.down.sql」、「000001_create_users_table.up.sql」が作成されるので、それぞれを以下のように修正します。
DROP TABLE IF EXISTS users ;
CREATE TABLE IF NOT EXISTS ` users ` (
` id ` int ( 11 ) NOT NULL AUTO_INCREMENT ,
` uid ` VARCHAR ( 191 ) NOT NULL ,
` name ` VARCHAR ( 191 ) NULL ,
` email ` VARCHAR ( 191 ) NULL ,
` created_at ` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ,
` updated_at ` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP ,
` deleted_at ` DATETIME NULL ,
UNIQUE INDEX ` users_email_key ` ( ` email ` ) ,
UNIQUE INDEX ` users_uid_key ` ( ` uid ` ) ,
PRIMARY KEY ( ` id ` )
) ;
これでdatabaseとマイグレーションの準備が整ったので、以下のコマンドを実行してコンテナを起動させます。
下図のように追加したDBのコンテナも追加されて起動すればOKです。
次に以下のコマンドを実行し、マイグレーションによりDB(MySQL)にusersテーブルが追加されていることも確認してみます。
$ docker compose exec db bash
$ mysql -u root --password= go_sample_root_password
$ use go_sample_db
$ show tables;
下図のようにDB(MySQL)にusersテーブルが追加されていればOKです。
ついでに以下のコマンドを実行し、usersテーブルのカラムも確認しておきます。
$ show columns from users ;
下図のようにusersテーブルのカラムが確認できればOKです。
最後にMySQLとコンテナから抜けるには以下のコマンドを実行します。
modelの追加およびusersテーブルの全レコードをJSON形式で返すAPIの作成
次に上記で追加したusersテーブル用のmodel(DBの対象テーブルと情報をやり取りするためのクラス的なもの)を追加するため、以下のコマンドを実行してディレクトリやファイルを作成します。
$ cd src
$ mkdir model
$ cd model
$ touch user.go
$ cd .. /..
そして、作成した「user.go」の中身は以下のように修正します。
package model
import (
"time"
"go_sample/database"
"errors"
)
// ユーザー情報の構造体
// usersテーブルのカラム名でjson出力するため、各項目に「`json:"" `」を設定します。
type User struct {
Id int `json:"id" `
Uid string `json:"uid" `
Name string `json:"name" `
Email string `json:"email" `
CreatedAt time.Time `json:"created_at" `
UpdatedAt time.Time `json:"updated_at" `
DeletedAt *time.Time `json:"deleted_at" `
}
// DBからusersテーブルの全レコードを取得
func GetUsersAll() (*[]User, error) {
db := database.GetDB()
var users *[]User
result := db.Find(&users)
if result.Error != nil {
return nil, result.Error
} else if result.RowsAffected == 0 {
return nil, errors.New("users not registerd" )
}
return users, nil
}
次にusersテーブルの全レコードをJSON形式で返すAPIを作成するため、まずは以下のコマンドを実行してコントローラーを作成します。
$ cd src/controller
$ touch users_controller.go
$ cd .. /..
そして、「users_controller.go」の中身は以下のように修正します。
package controller
import (
"github.com/gin-gonic/gin"
"go_sample/model"
"net/http"
)
// ユーザー一覧をJSON形式で出力
func UsersIndex(c *gin.Context) {
users, err := model.GetUsersAll()
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"error" : err.Error(),
})
return
}
c.JSON(http.StatusOK, users)
}
次にusersテーブルの全レコードをJSON形式で返すAPIを追加するため、「router.go」を以下のように修正します。
package router
import (
"github.com/gin-gonic/gin"
"go_sample/controller"
)
func Init() {
// routerの初期化
router := gin.Default()
// routerの設定
router.GET("/" , controller.Index)
// APIの設定
apiV1 := router.Group("/api/v1" )
apiV1.GET("/config" , controller.ConfigIndex)
apiV1.GET("/users" , controller.UsersIndex)
// routerを起動
router.Run(":3001" )
}
次に以下のコマンドを実行し、go.modファイルを修正します。
$ docker compose exec api go mod tidy
次に以下のコマンドを実行し、コンテナの再ビルドして起動させます。
$ docker compose down
$ docker compose build --no-cache
$ docker compose up -d
次にブラウザで「localhost/api/v1/users」にアクセスし、下図のようになればOKです。
seedとcmdの追加およびAPIの確認
次にseed(テーブルにテストデータを追加するためのファイル)とcmd(seedを実行したりするためのコマンド用のファイル)を追加するため、以下のコマンドを実行してディレクトリとファイルを作成します。
$ cd src
$ mkdir seed
$ cd seed
$ touch user_seeds.go
$ cd ..
$ mkdir cmd
$ cd cmd
$ touch seeder.go
$ touch down_migrate.go
$ cd .. /..
そして「user_seeds.go」、「seeder.go」、「down_migrate.go」はそれぞれ以下のように修正します。
package seed
import (
"time"
"go_sample/model"
"gorm.io/gorm"
)
func UserSeeds(db *gorm.DB) error {
// 1件目のデータ
user1 := model.User{
Uid: "uid:go_sample_1" ,
Name: "Sample_User_1" ,
Email: "sample1@example.com" ,
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
}
if err := db.Create(&user1).Error; err != nil {
return err
}
// 2件目のデータ
user2 := model.User{
Uid: "uid:go_sample_2" ,
Name: "Sample_User_2" ,
Email: "sample2@example.com" ,
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
}
if err := db.Create(&user2).Error; err != nil {
return err
}
return nil
}
package main
import (
"fmt"
"go_sample/config"
"go_sample/database"
"go_sample/seed"
)
func main() {
// Configの初期化
config.Init()
// DBの初期化
database.Init()
defer database.Close()
// DBの取得
db := database.GetDB()
// seedの実行
err := seed.UserSeeds(db)
if err != nil {
panic(err)
}
fmt.Println("Execution user_seeds.go !!" )
}
package main
import (
"fmt"
"go_sample/config"
"go_sample/database"
)
func main() {
// Configの初期化
config.Init()
// DBの初期化
database.Init()
defer database.Close()
// mの取得
m := database.GetM()
// マイグレーションのDownコマンド実行
err := m.Down()
if err != nil {
panic(err)
}
fmt.Println("Execution m.Down() !!" )
}
次に以下のコマンドを実行し、go.modファイルを修正します。
$ docker compose exec api go mod tidy
次に以下のコマンドを実行し、user_seeds.goを実行させてDBのusersテーブルにデータを作成します。
$ docker compose exec api go run cmd/seeder.go
次にブラウザで「localhost/api/v1/users」にアクセスし、下図のようにDBのusersテーブルから取得したデータがJSON形式で出力されればOKです。
userモデルにCRUDを作る
次にuserモデルにCRUD(作成「Create」、読み出し「Read」、更新「Update」、削除「Delete」)の処理を作り、各種DB操作を出来るようにします。
まずは「model/user.go」を以下のように修正します。
package model
import (
"time"
"go_sample/database"
"errors"
)
// ユーザー情報の構造体
// usersテーブルのカラム名でjson出力するため、各項目に「`json:"" `」を設定します。
type User struct {
Id int `json:"id" `
Uid string `json:"uid" `
Name string `json:"name" `
Email string `json:"email" `
CreatedAt time.Time `json:"created_at" `
UpdatedAt time.Time `json:"updated_at" `
DeletedAt *time.Time `json:"deleted_at" `
}
// DBからusersテーブルの全レコードを取得
func GetUsersAll() (*[]User, error) {
db := database.GetDB()
var users *[]User
result := db.Find(&users)
if result.Error != nil {
return nil, result.Error
} else if result.RowsAffected == 0 {
return nil, errors.New("users not registerd" )
}
return users, nil
}
// ユーザーを作成
func CreateUser(r User) error {
db := database.GetDB()
// トランザクションの開始
tx := db.Begin()
user := User{
Uid: r.Uid,
Name: r.Name,
Email: r.Email,
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
}
if err := tx.Create(&user).Error; err != nil {
// エラーの場合はロールバック
tx.Rollback()
return err
}
// コミットしてトランザクションを終了
tx.Commit()
return nil
}
// 対象のユーザーを1件取得
func GetUser(id int) (*User, error) {
db := database.GetDB()
var user *User
result := db.First(&user, id)
if result.Error != nil {
return nil, result.Error
}
return user, nil
}
// ユーザー情報を更新
func UpdateUser(id int, r User) error {
db := database.GetDB()
// トランザクションの開始
tx := db.Begin()
// nil値項目の更新なし、updated_atの更新あり
err := tx.Updates(&User{
Id: id,
Name: r.Name,
Email: r.Email,
}).Error
if err != nil {
// エラーの場合はロールバック
tx.Rollback()
return err
}
// コミットしてトランザクションを終了
tx.Commit()
return nil
}
// ユーザー削除
func DeleteUser(id int) error {
db := database.GetDB()
// トランザクションの開始
tx := db.Begin()
// 対象ユーザーを物理削除
if err := tx.Delete(&User{}, id).Error; err != nil {
// エラーの場合はロールバック
tx.Rollback()
return err
}
// コミットしてトランザクションを終了
tx.Commit()
return nil
}
次に「controller/users_controller.go」を以下のように修正します。
package controller
import (
"github.com/gin-gonic/gin"
"go_sample/model"
"net/http"
"strconv"
)
// ユーザー一覧をJSON形式で出力
func UsersIndex(c *gin.Context) {
users, err := model.GetUsersAll()
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"error" : err.Error(),
})
return
}
c.JSON(http.StatusOK, users)
}
// ユーザーを作成
func UserCreate(c *gin.Context) {
// リクエストボディをバインド
var req model.User
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"error" : err.Error(),
})
return
}
if err := model.CreateUser(req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"error" : err.Error(),
})
return
}
c.JSON(http.StatusOK, req)
}
// ユーザー1件をJSON形式で出力
func UserShow(c *gin.Context) {
// パラメータ「:id」を数値変換
id, err := strconv.Atoi(c.Param("id" ))
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"error" : err.Error(),
})
return
}
user, err := model.GetUser(id)
if err != nil {
c.JSON(http.StatusNotFound, gin.H{
"message" : err.Error(),
})
return
}
c.JSON(http.StatusOK, user)
}
// ユーザー情報を更新
func UserUpdate(c *gin.Context) {
// パラメータ「:id」を数値変換
id, err := strconv.Atoi(c.Param("id" ))
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"error" : err.Error(),
})
return
}
// リクエストボディをバインド
var req model.User
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"error" : err.Error(),
})
return
}
if err := model.UpdateUser(id, req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"error" : err.Error(),
})
return
}
c.JSON(http.StatusOK, req)
}
// ユーザーを削除
func UserDelete(c *gin.Context) {
// パラメータ「:id」を数値変換
id, err := strconv.Atoi(c.Param("id" ))
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"error" : err.Error(),
})
return
}
if err := model.DeleteUser(id); err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"error" : err.Error(),
})
return
}
c.JSON(http.StatusOK, gin.H{
"message" : "user(" + strconv.Itoa(id) + ") is deleted" ,
})
}
次に「router/router.go」を以下のように修正します。
package router
import (
"github.com/gin-gonic/gin"
"go_sample/controller"
)
func Init() {
// routerの初期化
router := gin.Default()
// routerの設定
router.GET("/" , controller.Index)
// APIの設定
apiV1 := router.Group("/api/v1" )
apiV1.GET("/config" , controller.ConfigIndex)
apiV1.GET("/users" , controller.UsersIndex)
// ユーザーのCRUD用APIを追加
apiV1.POST("/users" , controller.UserCreate)
apiV1.GET("/users/:id" , controller.UserShow)
apiV1.PATCH("/users/:id" , controller.UserUpdate)
apiV1.DELETE("/users/:id" , controller.UserDelete)
// routerを起動
router.Run(":3001" )
}
次に以下のコマンドを実行し、go.modファイルを修正します。
$ docker compose exec api go mod tidy
PostmanでAPIを検証
これでuserモデルにCRUDを追加できたので、Postman(APIテストを行うためのツール)を使ってAPIを実行して試してみます。
尚、Postman についてここでは詳しく解説しませんが、以下の関連記事にある「17.5 Postmanの登録方法」の部分で解説している ので、まだ使ったことがない方は参考にしてみて下さい。
関連記事
Createの検証
では最初にCreateのAPIを検証しますが、Postmanでワークスペースを開き、画面左上の「+」をクリックして新しいコレクションを作成します。
次にコレクション名を付けますが、今回は「go_sample Collection」とします。 半角スペースが含まれていると後でファイルをエクスポートして使用する際に困るので、「go_sample_collection」のようにして下さい。
次に作成したコレクション名の左にある「>」をクリックしてフォルダを開き、「Add a request」をクリックします。
次にリクエスト名を「Create User Request」、メソッドを「POST」、エンドポイントを「http://localhost/api/v1/users」に設定します。
次に画面中央のタブ「Body」をクリック後、「raw」と「JSON」を選択し、入力欄には登録したいユーザー情報をJSON形式で記述します。
そして、画面右上の「Send」をクリックするとAPIが実行され、画面中央のStatusが「200 OK」となれば正常終了したのでOKです。
最後にブラウザで「http://localhost/api/v1/users」にアクセスしてユーザー一覧を確認し、上記で指定した条件でユーザーが新規作成されていればOKです。
Readの検証
次にReadのAPIを検証しますが、まずはコレクション名の右にある「◦◦◦」からメニューを開き、「Add request」をクリックして新しいリクエストを作成します。
次にリクエスト名を「Show User Request」、メソッドを「GET」、エンドポイントを「http://localhost/api/v1/users/:id」に設定後、画面下にパラメータ「id」の項目が表示されるので、Valueには上記Create APIで作成したデータを取得できるように「3」を設定します。
リクエストの設定完了後、画面右上の「Send」をクリックするとAPIが実行され、画面中央のStatusが「200 OK」、画面下のフィールドに先ほどCreate APIで作成したデータがJSON形式で表示されればOKです。
Updateを検証
次にUpdateのAPIを検証しますが、先ほどと同様に新しいリクエストを作成後、リクエスト名に「Update User Request」、メソッドに「PATCH」、エンドポイントに「http://localhost/api/v1/users/:id」、パラメータの値に「3」を設定します。
次に画面中央のタブ「Body」をクリック後、「raw」と「JSON」を選択し、入力欄には更新したい項目についてJSON形式で記述しますが、今回の例では、Create APIで作成したユーザーの名前を「Sample_User_3」に更新します。
リクエストの設定完了後、画面右上の「Send」をクリックするとAPIが実行され、画面中央のStatusが「200 OK」になればOKです。
最後にブラウザで「http://localhost/api/v1/users」にアクセスしてユーザー一覧を確認し、ユーザーの名前と更新日が更新されていればOKです。
Deleteを検証
次にDeleteのAPIを検証しますが、先ほどと同様に新しいリクエストを作成後、リクエスト名に「Delete User Request」、メソッドに「DELETE」、エンドポイントに「http://localhost/api/v1/users/:id」、パラメータの値に「3」を設定します。
リクエストの設定完了後、画面右上の「Send」をクリックするとAPIが実行され、画面中央のStatusが「200 OK」、画面下のフィールドに削除時のメッセージが表示されればOKです。
最後にブラウザで「http://localhost/api/v1/users」にアクセスしてユーザー一覧を確認し、Create APIで作成したユーザーが削除されて表示されなければOKです。
PostmanでAPIのテストコードを作成
次にPostmanでAPIのテストコードを作成します。
Create User Requestのテスト
まずはCreate User Requestのタブ「Pre-request Script」をクリックし、リクエスト実行前に実行しておきたい処理を記述します。
今回は例として、リクエスト実行後にユーザーのデータ件数が1件増えることをテストするため、事前にユーザーのデータ件数を取得し、変数「beforeDataCount」に格納しておきます。
尚、ユーザーのデータ件数の取得方法については、ユーザー一覧を取得するAPI「http://localhost/api/v1/users」を実行し、リクエスト結果のデータからユーザーの件数を取得しています。
・Pre-request Scriptに記述するコード
pm. sendRequest ( "http://localhost/api/v1/users" , function ( err , response ) {
const beforeDataCount = response. json ( ) . length
pm. variables. set ( "beforeDataCount" , beforeDataCount) ;
} ) ;
※コードはjavascriptで記述します。 また、データ件数を変数に格納するには、「pm.variables.set(変数名, 変数にセットする値)」を使います。
次にタブ「Tests」をクリックし、リクエスト実行後に検証したいテスト内容を記述します。
今回の例では、リクエスト結果のステータスコードが200であることと、リクエスト実行後のユーザーのデータ件数が1件増えていることを検証します。
・Testsに記述するコード
pm. test ( "Status code is 200" , function ( ) {
pm. response. to. have. status ( 200 ) ;
} ) ;
pm. sendRequest ( "http://localhost/api/v1/users" , function ( err , response ) {
const beforeDataCount = pm. variables. get ( "beforeDataCount" ) ;
const afterDataCount = response. json ( ) . length;
pm. test ( "ユーザーのデータ件数が1件増加" , function ( ) {
pm. expect ( afterDataCount) . to. eql ( beforeDataCount + 1 ) ;
} ) ;
} ) ;
※事前に設定しておいた変数から値を取り出すには「pm.variables.get(変数名)」を使います。
次に画面右上の「Send」をクリックし、APIとテストを実行します。
実行後、画面下のタブ「Test Results」をクリックし、テスト結果を確認できますが、テスト項目のステータスが「PASS」なら正常終了なのでOKです。
Show User Requestのテスト
次にShow User Requestではエンドポイントに「:id」が含まれていて何か値を指定する必要がありますが、APIの実行でデータの作成や削除を繰り返すと、対象データのidの値は毎回変わってしまうので注意 が必要です。
そのため、リクエスト実行前の処理でidに指定する値を取得して変数に格納後、その変数の値をパラメータの「:id」に指定する ことで、idの値が変わる問題に対処します。
ではShow User Requestのタブ「Params」をクリックし、idのValueには変数「paramId」を設定しますが、変数の値を利用する場合は「{{}}」で囲んで「{{paramId}}」を設定 します。
次にタブ「Pre-request Script」をクリック後、リクエスト実行前の処理として、ユーザー一覧を取得するAPIを実行し、レスポンス結果から取得したデータの最後のデータのid(直近で作成されたデータのid)を変数「paramId」に格納します。
・Pre-request Scriptに記述するコード
pm. sendRequest ( "http://localhost/api/v1/users" , function ( err , response ) {
const lastUserData = response. json ( ) . pop ( )
pm. variables. set ( "paramId" , lastUserData. id) ;
} ) ;
次にタブ「Tests」をクリックし、今回の例ではリクエスト結果のステータスコードが200であることと、リクエスト結果のデータがCreate User Requestで作成したデータの値と一致していることを検証します。
・Testsに記述するコード
pm. test ( "Status code is 200" , function ( ) {
pm. response. to. have. status ( 200 ) ;
} ) ;
pm. test ( "id == paramId" , function ( ) {
const paramId = pm. variables. get ( "paramId" ) ;
pm. expect ( pm. response. json ( ) . id) . to. eql ( paramId) ;
} ) ;
pm. test ( "uid == uid:test3" , function ( ) {
pm. expect ( pm. response. json ( ) . uid) . to. eql ( "uid:test3" ) ;
} ) ;
pm. test ( "name == test3" , function ( ) {
pm. expect ( pm. response. json ( ) . name) . to. eql ( "test3" ) ;
} ) ;
pm. test ( "email == test3@example.com" , function ( ) {
pm. expect ( pm. response. json ( ) . email) . to. eql ( "test3@example.com" ) ;
} ) ;
pm. test ( "created_at is not null" , function ( ) {
pm. expect ( pm. response. json ( ) . created_at) . not. eql ( null ) ;
} ) ;
pm. test ( "updated_at is not null" , function ( ) {
pm. expect ( pm. response. json ( ) . updated_at) . not. eql ( null ) ;
} ) ;
pm. test ( "deleted_at is null" , function ( ) {
pm. expect ( pm. response. json ( ) . deleted_at) . to. eql ( null ) ;
} ) ;
次に画面右上の「Send」をクリックしてAPIとテストを実行後、画面下のタブ「Test Results」からテスト結果を確認し、全てのテスト項目が「PASS」ならOKです。
Update User Requestのテスト
次にUpdate User Requestも上記と同様にまずはタブ「Params」からidのValueに「paramId」を設定します。
次にタブ「Pre-request Script」をクリック後、リクエスト実行前の処理として、パラメータに設定するidの値と、idに紐付くユーザーのデータを取得し、それぞれを変数に格納します。
・Pre-request Scriptに記述するコード
pm. sendRequest ( "http://localhost/api/v1/users" , function ( err , response ) {
const lastUserData = response. json ( ) . pop ( )
pm. variables. set ( "paramId" , lastUserData. id) ;
pm. variables. set ( "beforeUser" , lastUserData) ;
const sleep = waitTime => new Promise ( resolve => setTimeout ( resolve, waitTime) ) ;
sleep ( 1000 ) ;
} ) ;
※データ更新時にupdated_atの値も更新されることを検証するため、sleep関数を定義して1秒間だけ待機処理をさせ、リクエストが実行されるまでの時間を少しずらしています。(これをしないと処理が即時実行されてcreated_atとupdated_atが同じ値になることがあるので注意)
次にタブ「Tests」をクリックし、今回の例ではリクエスト結果のステータスコードが200であることと、リクエスト実行後に対象の項目だけ指定した値で更新されていることを検証します。
・Testsに記述するコード
pm. test ( "Status code is 200" , function ( ) {
pm. response. to. have. status ( 200 ) ;
} ) ;
pm. sendRequest ( "http://localhost/api/v1/users" , function ( err , response ) {
const beforeUser = pm. variables. get ( "beforeUser" ) ;
const afterUser = response. json ( ) . pop ( )
pm. test ( "リクエスト実行前後でidが更新されていないこと" , function ( ) {
pm. expect ( afterUser. id) . to. eql ( beforeUser. id) ;
} ) ;
pm. test ( "リクエスト実行前後でuidが更新されていないこと" , function ( ) {
pm. expect ( afterUser. uid) . to. eql ( beforeUser. uid) ;
} ) ;
pm. test ( "リクエスト実行後にnameが「Sample_User_3」更新されていること" , function ( ) {
pm. expect ( afterUser. name) . to. eql ( "Sample_User_3" ) ;
} ) ;
pm. test ( "リクエスト実行前後でemailが更新されていないこと" , function ( ) {
pm. expect ( afterUser. email) . to. eql ( beforeUser. email) ;
} ) ;
pm. test ( "リクエスト実行後はupdated_atが更新されていること" , function ( ) {
pm. expect ( afterUser. updated_at > beforeUser. updated_at) . to. eql ( true ) ;
} ) ;
pm. test ( "リクエスト実行後にdeleted_atがnullであること" , function ( ) {
pm. expect ( afterUser. deleted_at) . to. eql ( null ) ;
} ) ;
} ) ;
次に画面右上の「Send」をクリックしてAPIとテストを実行後、画面下のタブ「Test Results」からテスト結果を確認し、全てのテスト項目が「PASS」ならOKです。
Delete User Requestのテスト
次にDelete User Requestも上記と同様にまずはタブ「Params」からidのValueに「paramId」を設定します。
次にタブ「Pre-request Script」をクリック後、リクエスト実行前の処理として、ユーザー一覧のデータ件数とパラメータに設定するidの値を取得し、それぞれを変数に格納します。
・Pre-request Scriptに記述するコード
pm. sendRequest ( "http://localhost/api/v1/users" , function ( err , response ) {
const beforeDataCount = response. json ( ) . length
const lastUserData = response. json ( ) . pop ( )
pm. variables. set ( "beforeDataCount" , beforeDataCount) ;
pm. variables. set ( "paramId" , lastUserData. id) ;
} ) ;
次にタブ「Tests」をクリックし、今回の例ではリクエスト結果のステータスコードが200であることと、リクエスト実行後のユーザーのデータ件数が1件減っていることを検証します。
・Testsに記述するコード
pm. test ( "Status code is 200" , function ( ) {
pm. response. to. have. status ( 200 ) ;
} ) ;
pm. sendRequest ( "http://localhost/api/v1/users" , function ( err , response ) {
const beforeDataCount = pm. variables. get ( "beforeDataCount" ) ;
const afterDataCount = response. json ( ) . length;
pm. test ( "ユーザーのデータ件数が1件減少" , function ( ) {
pm. expect ( afterDataCount) . to. eql ( beforeDataCount - 1 ) ;
} ) ;
} ) ;
次に画面右上の「Send」をクリックしてAPIとテストを実行後、画面下のタブ「Test Results」からテスト結果を確認し、全てのテスト項目が「PASS」ならOKです。
コレクション内の全てのAPIとテストをまとめて実行
上記で各APIのテストコードが書けたので、次はコレクション内の全てのテストをまとめて実行させて確認します。
まずは画面左にあるコレクション名をクリックします。
次に画面右上にある「Run」をクリックします。
次に新しいタブ「Runner」が開くので、画面下にある「Run [コレクション名]」をクリックして処理を実行します。
処理実行後、コレクション内の全てのAPIとテストが実行されるので、実行結果が全てPASSすればOKです。
Postman CLIからコレクションのテストを実行
次にコマンドから上記のコレクションのテストを実行できるようにするため、Postman CLI をインストールします。
※PostmanのCLIには「Newman」というのもありますが、私の環境では上手くいかなかった(http://localhostへのリクエストが失敗する)ので、Postman CLIをインストールして使用することにしました。
Postman CLIをインストールするには、公式サイトにあるPostman CLIのページ を開き、使用しているデバイスに応じて、対応するコマンドを実行すればインストール可能です。
私の場合は現在Intel版のMacBookを利用しているので、以下のコマンドを実行してインストール しました。
$ curl -o- "https://dl-cli.pstmn.io/install/osx_64.sh" | sh
インストール後、以下のコマンドを実行してバージョンを確認します。
コマンド実行後、以下のようにバージョンが表示されればpostmanコマンドが使用可能です。
次に上記で作成したコレクションのデータをエクスポートするため、コレクション画面の右上にある「◦◦◦」からメニューを開き、「Export」をクリックします。
次にポップアップ画面が表示されるので、画面右下の「Export」をクリックします。
これでコレクションのjsonファイルがダウンロードできます。
次に以下のコマンドを実行し、上記で取得したファイルを配置するディレクトリを作成します。
$ cd src
$ mkdir postman
$ cd ..
次に上記でダウンロードしたコレクションのファイルをコピーし、ディレクトリ「go_sample/src/postman」配下に格納します。
これで準備ができたので、以下のコマンドを実行し、コレクションのテストを実行してみます。
$ postman collection run src/postman/go_sample_collection.postman_collection.json
コマンド実行後、以下のようにコレクションが実行され、全てのテストが正常終了すればOKです。
GitHub ActionsでCIの構築(テストの自動化)
ここまででコマンドからPostmanのテストを実行できるようになりましたが、最後にCI(Continuous Integration)を構築し、テストの自動化 を行います。
CIを構築するツールは色々ありますが、現在はGitHub Actionsで構築するのがトレンド なので、今回はGitHubにコードをプッシュした際に、GitHub ActionsからPostmanのテストを自動的に実行できるようにします。
※以下ではGitHubを既に利用している前提で話を進めます。
まずは以下のコマンドを実行し、GitHub Actions用のディレクトリとファイルを作成します。
$ mkdir -p .github/workflows
$ cd .github/workflows
$ touch actions.yml
$ cd .. /..
次にファイル「actions.yml」を以下のように修正します。
name: go_sample github actions
on:
push:
jobs:
test:
name: Run Postman Api Tests
runs-on: ubuntu-latest
# 使用する環境変数定義の指定
environment: dev
# 処理の実行
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Postman CLI install
run: curl -o- "https://dl-cli.pstmn.io/install/linux64.sh" | sh
# .envの作成
- name: Create .env
run: touch .env
- name: Add environment variable to .env
run: |
echo ENV=${{secrets.ENV}} >> .env
echo TZ=${{secrets.TZ}} >> .env
echo MYSQL_DATABASE=${{secrets.MYSQL_DATABASE}} >> .env
echo MYSQL_USER=${{secrets.MYSQL_USER}} >> .env
echo MYSQL_PASSWORD=${{secrets.MYSQL_PASSWORD}} >> .env
echo MYSQL_ROOT_PASSWORD=${{secrets.MYSQL_ROOT_PASSWORD}} >> .env
# コンテナのビルドと起動
- name: docker compose build --no-cache
run: docker compose build --no-cache
- name: docker compose up -d
run: docker compose up -d
# コンテナが立ち上がり切るまでの待機処理
- name: sleeep 30
run: sleep 30
# seederの実行
- name: docker compose exec api go run cmd/seeder.go
run: docker compose exec api go run cmd/seeder.go
# テストの実行
- name: postman collection run
run: postman collection run src/postman/go_sample_collection.postman_collection.json
次にGitHubの対象リポジトリに環境変数を設定するため、リポジトリ画面上のメニューから「Settings」をクリックします。
次に画面左のメニューから「Environments」をクリック後、画面右上の「New environment」をクリックします。
次に環境変数定義の名前を入力し、「Configure environment」をクリックします。
※今回は例として「dev」としています。
これで環境変数の定義が作成されたので、画面下にあるEnvironment secrets(機密情報用の環境変数)の「+ Add secret」をクリックします。
次にポップアップが表示されるので、環境変数の名前と値を入力後、右下の「Add secret」をクリックすると環境変数の登録が可能です。
上記の方法にて、「.env」ファイルに定義していた環境変数を全て登録します。
※環境変数を登録後、ymlファイルの「environment:」で環境変数の定義を指定(今回の例だとdevを指定)すると、「${{secrets.ENV}}」のようにして登録した環境変数にアクセス可能です。
これで準備が整ったので、GitHubにコードをプッシュ してみて下さい。
プッシュ後、GtiHubのリポジトリ画面上のメニューから「Actions」をクリックすると、実行されたワークフローを確認 できます。
さらにワークフローの詳細を確認したい場合は、画面中央にある対象のコミットメッセージをクリックします。
次に画面中央のジョブ名をクリックします。
これでジョブの実行中の処理を確認可能です。
ジョブの処理が終了後、画面左のステータスのマークに緑色のチェックがつけば正常終了 なのでOKです。(エラーの場合は赤色の×マークになります)
最後に
今回はGolangでAPIを開発する方法についてまとめました。
Railsと比べると圧倒的に情報が少ないため、Golangの基礎的な部分を学ぶのも一苦労しましたが、基本的な部分はこの記事にてある程度まとめれたと思います。
これからGolangでAPIの開発を始めてみたいという方は、ぜひ参考にしてみて下さい。
各種SNSなど
各種SNSなど、チャンネル登録やフォローをしていただけると励みになるので、よければぜひお願いします!
The following two tabs change content below.
SEを5年経験後、全くの未経験ながら思い切ってブロガーに転身し、月間13万PVを達成。その後コロナの影響も受け、以前から興味があったWeb系エンジニアへのキャリアチェンジを決意。現在はWeb系エンジニアとして働きながら、プロゲーマーとしても活躍できるように活動中。
コメントを残す