go-gin + mysql + gorm + Dockerで開発用APIサーバーを立ててみた

簡易なウェブアプリが作りたくて、そのときのメモです。
go-ginでAPIサーバーを立ててデータベースと通信できるようにします。

[Github Repository](https://github.com/greenteabiscuit/next-gin-mysql)を作りました。

https://github.com/greenteabiscuit/next-gin-mysql

## OS

- macOS Catalina

## 使う技術スタック

- gin (ウェブサーバー)
- dlv (デバッグ)
- air (ホットリロード)
- gorm (ORMライブラリ)
- mysql
- golang-migrate (マイグレーション)
- docker

## あると便利なアプリ

- postman (rest api通信確認)
- goland (デバッグ、開発)
- sequel pro (データベース確認)

## ディレクトリ構造

最終的には以下のようになっています。

```
.
├── backend
│   ├── Dockerfile.local
│   ├── article
│   │   └── article.go
│   ├── go.mod
│   ├── go.sum
│   ├── handler
│   │   ├── articleFunc.go
│   │   └── userFunc.go
│   ├── lib
│   │   └── sql_handler.go
│   ├── main.go
│   ├── migration
│   │   ├── main.go
│   │   └── migrations
│   │       ├── 1_articles.down.sql
│   │       ├── 1_articles.up.sql
│   │       ├── 2_users.down.sql
│   │       └── 2_users.up.sql
│   ├── tmp
│   │   └── main
│   └── user
│       └── user.go
├── docker-compose.yml
└── mysql
    ├── Dockerfile
    └── my.cnf

9 directories, 18 files
```

## 気をつけるところ

`github.com/自分のアカウント/自分のレポジトリ/main.goが存在するフォルダ`のところは各自書き換えてください。

# ディレクトリ

`next-gin-mysql`という名前で立ち上げます。(`next`をフロントエンドとして立ち上げたいのですが、記事の長さ的にそれは次回にします）

```
$ mkdir next-gin-mysql
```

## バックエンド側

まずディレクトリを作ります。

```
next-gin-mysql $ mkdir backend
```

## go moduleの用意

go modulesの用意をします。
`github.com/自分のアカウント/自分のレポジトリ/main.goが存在するフォルダ`で初期化するとやりやすいと思います。

```
next-gin-mysql/backend $ go mod init github.com/greenteabiscuit/next-gin-mysql/backend

next-gin-mysql/backend $ go get -u github.com/gin-gonic/gin
```

これで ginとその依存パッケージがダウンロードされます。

## それぞれのファイルの用意

サーバーを立ち上げる`main.go`です。データベース用の`lib`、記事とユーザーのモデル、そしてハンドラはあとで書きます。

```main.go
package main

import (
	"os"
	"time"

	"github.com/gin-gonic/gin"
	"github.com/greenteabiscuit/next-gin-mysql/backend/article"
	"github.com/greenteabiscuit/next-gin-mysql/backend/handler"
	"github.com/greenteabiscuit/next-gin-mysql/backend/lib"
	"github.com/greenteabiscuit/next-gin-mysql/backend/user"
	"github.com/joho/godotenv"

	"github.com/gin-contrib/cors"
)

func main() {
	if os.Getenv("USE_HEROKU") != "1" {
		err := godotenv.Load()
		if err != nil {
			panic(err)
		}
	}

	article := article.New()
	user := user.New()

	lib.DBOpen()
	defer lib.DBClose()

	r := gin.Default()

	r.Use(cors.New(cors.Config{
		AllowOrigins: []string{
			"http://localhost:3000",
		},
		AllowMethods: []string{
			"POST",
			"GET",
			"OPTIONS",
		},
		AllowHeaders: []string{
			"Access-Control-Allow-Credentials",
			"Access-Control-Allow-Headers",
			"Content-Type",
			"Content-Length",
			"Accept-Encoding",
			"Authorization",
		},
		AllowCredentials: true,
		MaxAge:           24 * time.Hour,
	}))

	r.GET("/article", handler.ArticlesGet(article))
	r.POST("/article", handler.ArticlePost(article))
	r.POST("/user/login", handler.UserPost(user))

	r.Run(os.Getenv("HTTP_HOST") + ":" + os.Getenv("HTTP_PORT")) // listen and serve on 0.0.0.0:8080
}

```

記事を扱うarticleのハンドラパッケージです。

```handler/articleFunc.go
package handler

import (
	"net/http"

	"github.com/greenteabiscuit/next-gin-mysql/backend/article"

	"github.com/gin-gonic/gin"
)

func ArticlesGet(articles *article.Articles) gin.HandlerFunc {
	return func(c *gin.Context) {
		result := articles.GetAll()
		c.JSON(http.StatusOK, result)
	}
}

type ArticlePostRequest struct {
	Title       string `json:"title"`
	Description string `json:"description"`
}

func ArticlePost(post *article.Articles) gin.HandlerFunc {
	return func(c *gin.Context) {
		requestBody := ArticlePostRequest{}
		c.Bind(&requestBody)

		item := article.Article{
			Title:       requestBody.Title,
			Description: requestBody.Description,
		}
		post.Add(item)

		c.Status(http.StatusNoContent)
	}
}

```

userのハンドラ関数は以下です。

```handler/userFunc.go
package handler

import (
	"net/http"

	"github.com/greenteabiscuit/next-gin-mysql/backend/user"

	"github.com/gin-gonic/gin"
)

func UsersGet(users *user.Users) gin.HandlerFunc {
	return func(c *gin.Context) {
		result := users.GetAll()
		c.JSON(http.StatusOK, result)
	}
}

type UserPostRequest struct {
	Username string `json:"username"`
	Password string `json:"password"`
}

func UserPost(post *user.Users) gin.HandlerFunc {
	return func(c *gin.Context) {
		requestBody := UserPostRequest{}
		c.Bind(&requestBody)

		item := user.User{
			Username: requestBody.Username,
			Password: requestBody.Password,
		}
		post.Add(item)

		c.Status(http.StatusNoContent)
	}
}
```

articleの構造体などを定義するファイルです。

```article/article.go
package article

import (
	"fmt"

	"github.com/greenteabiscuit/next-gin-mysql/backend/lib"
)

type Article struct {
	Title       string `json:"title"`
	Description string `json:"description"`
}

type Articles struct {
	Items []Article
}

func New() *Articles {
	return &Articles{}
}

func (r *Articles) Add(a Article) {
	r.Items = append(r.Items, a)
	db := lib.GetDBConn().DB
	if err := db.Create(a).Error; err != nil {
		fmt.Println("err!")
	}
}

func (r *Articles) GetAll() []Article {
	db := lib.GetDBConn().DB
	var articles []Article
	if err := db.Find(&articles).Error; err != nil {
		return nil
	}
	return articles
}

```

userの構造体をまとめたファイルです。ちょっとここはテキトーなのでもう少し直したい、、、

```user/user.go
package user

import (
	"fmt"

	"github.com/greenteabiscuit/next-gin-mysql/backend/lib"
)

type User struct {
	Username string `json:"username"`
	Password string `json:"password"`
}

type Users struct {
	Items []User
}

func New() *Users {
	return &Users{}
}

func (r *Users) Add(a User) {
	r.Items = append(r.Items, a)
	db := lib.GetDBConn().DB
	if err := db.Create(a).Error; err != nil {
		fmt.Println("err!")
	}
}

func (r *Users) GetAll() []User {
	db := lib.GetDBConn().DB
	var users []User
	if err := db.Find(&users).Error; err != nil {
		return nil
	}
	return users
}
```

mysqlと接続を行う `sql_handler.go`も追加します。

```lib/sql_handler.go
package lib

import (
	"fmt"
	"os"
	"time"

	"gorm.io/gorm"

	"gorm.io/driver/mysql"
)

// SQLHandler ...
type SQLHandler struct {
	DB  *gorm.DB
	Err error
}

var dbConn *SQLHandler

// DBOpen は DB connectionを張る。
func DBOpen() {
	dbConn = NewSQLHandler()
}

// DBClose は DB connectionを張る。
func DBClose() {
	sqlDB, _ := dbConn.DB.DB()
	sqlDB.Close()
}

// NewSQLHandler ...
func NewSQLHandler() *SQLHandler {
	user := os.Getenv("DB_USERNAME")
	password := os.Getenv("DB_PASSWORD")
	host := os.Getenv("DB_HOST")
	port := os.Getenv("DB_PORT")
	dbName := os.Getenv("DB_DATABASE")
	fmt.Println(user, password, host, port)

	var db *gorm.DB
	var err error
	// Todo: USE_HEROKU = 1のときと場合分け
	if os.Getenv("USE_HEROKU") != "1" {
		dsn := user + ":" + password + "@tcp(" + host + ":" + port + ")/" + dbName + "?parseTime=true&loc=Asia%2FTokyo"
		db, err = gorm.Open(mysql.Open(dsn), &gorm.Config{})
		if err != nil {
			panic(err)
		}
	} /*else {
		var (
			instanceConnectionName = os.Getenv("DB_CONNECTION_NAME") // e.g. 'project:region:instance'
		)
		dbURI := fmt.Sprintf("%s:%s@unix(/cloudsql/%s)/%s?parseTime=true", user, password, instanceConnectionName, database)
		// dbPool is the pool of database connections.
		db, err = gorm.Open(mysql.Open(dbURI), &gorm.Config{})
		if err != nil {
			panic(err)
		}
	}*/

	sqlDB, _ := db.DB()
	//コネクションプールの最大接続数を設定。
	sqlDB.SetMaxIdleConns(100)
	//接続の最大数を設定。 nに0以下の値を設定で、接続数は無制限。
	sqlDB.SetMaxOpenConns(100)
	//接続の再利用が可能な時間を設定。dに0以下の値を設定で、ずっと再利用可能。
	sqlDB.SetConnMaxLifetime(100 * time.Second)

	sqlHandler := new(SQLHandler)
	db.Logger.LogMode(4)
	sqlHandler.DB = db

	return sqlHandler
}

// GetDBConn ...
func GetDBConn() *SQLHandler {
	return dbConn
}

// BeginTransaction ...
func BeginTransaction() *gorm.DB {
	dbConn.DB = dbConn.DB.Begin()
	return dbConn.DB
}

// Rollback ...
func RollBack() {
	dbConn.DB.Rollback()
}

```

## ローカル開発環境の整備

docker-composeで一発でサーバーをすべて立ち上げるようにします。このときについでに mysqlの準備とデバッグサーバーの準備もしておきます。

```docker-compose.yml
version: '3'
services:
    go:
        build:
            context: ./backend
            dockerfile: Dockerfile.local
        volumes:
            - ./backend:/go/src/backend
        working_dir: /go/src/backend
        environment:
            TZ: Asia/Tokyo
        ports:
            - 8080:8080
            - 2345:2345
        security_opt: 
            - apparmor:unconfined
        cap_add: 
            - SYS_PTRACE

    mysql:
        build: ./mysql
        environment:
            TZ: Asia/Tokyo
            MYSQL_ROOT_PASSWORD: root
            MYSQL_DATABASE: backend
        ports:
            - 13306:3306
        volumes:
            - mysql_volume:/var/lib/mysql
    
volumes:
    mysql_volume:

```

go用のDockerfileです。今後デプロイ用のDockerfileと分けるかもしれないので、一応Dockerfile.localにしておきます。

```backend/Dockerfile.local
FROM golang:1.15.2
COPY . /go/src/sample
WORKDIR /go/src/sample
RUN go get -u github.com/cosmtrek/air
RUN go get -u github.com/go-delve/delve/cmd/dlv
CMD ["air", "-c", ".air.toml"]**
```

ホットリロードするために、airを使います。
airの設定ファイルである`.air.toml`は[airの本家レポジトリ](https://github.com/cosmtrek/air)から引っ張ってくることができます。

```backend/.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]
# Just plain old shell command. You could use `make` as well.
cmd = "go build -o ./tmp/main ."
# Binary file yields from `cmd`.
bin = "tmp/main"
# Customize binary.
full_bin = "APP_ENV=dev APP_USER=air /go/bin/dlv exec ./tmp/main --headless=true --listen=:2345 --api-version=2 --accept-multiclient"
# Watch these filename extensions.
include_ext = ["go", "tpl", "tmpl", "html"]
# Ignore these filename extensions or directories.
exclude_dir = ["assets", "tmp", "vendor", "frontend/node_modules", "storage"]
# Watch these directories if you specified.
include_dir = []
# Exclude files.
exclude_file = []
# Exclude unchanged files.
exclude_unchanged = true
# This log file places in your tmp_dir.
log = "air.log"
# It's not necessary to trigger build each time file changes if it's too frequent.
delay = 1000 # 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 # ms
[log]
# Show log time
time = 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
```

mysqlの設定ファイルです。

```mysql/my.cnf
# MySQLサーバーへの設定
[mysqld]
# 文字コード/照合順序の設定
character_set_server=utf8mb4
collation_server=utf8mb4_bin

# タイムゾーンの設定
default_time_zone=SYSTEM
log_timestamps=SYSTEM

# デフォルト認証プラグインの設定
default_authentication_plugin=mysql_native_password

# mysqlオプションの設定
[mysql]
# 文字コードの設定
default_character_set=utf8mb4

# mysqlクライアントツールの設定
[client]
# 文字コードの設定
default_character_set=utf8mb4
```

mysqlのDockerfileです。

```mysql/Dockerfile
FROM mysql:8.0.21
# FROM mysql@sha256:77b7e09c906615c1bb59b2e9d7703f728b1186a5a70e547ce2f1079ef4c1c5ca

RUN echo "USE mysql;" > /docker-entrypoint-initdb.d/timezones.sql &&  mysql_tzinfo_to_sql /usr/share/zoneinfo >> /docker-entrypoint-initdb.d/timezones.sql

COPY ./my.cnf /etc/mysql/conf.d/my.cnf
```

環境変数も設定しておきます。本番環境では見せないようにしましょう。
`DB_HOST`はコンテナの名前で選ぶので、`mysql`にしてあります。

```backend/.env
HTTP_HOST=""
HTTP_PORT=8080
DB_HOST="mysql"
DB_PORT=3306
DB_USERNAME=root
DB_PASSWORD=root
DB_DATABASE=backend
```

## 立ち上げてみる

サーバーを立ち上げてみましょう。以下のようにエラーが起きずに `2345`が出てきたらとりあえずOKです。

```
docker-compose up --build

mysql_1  | 2021-05-04T16:29:33.735276+09:00 0 [Warning] [MY-011810] [Server] Insecure configuration for --pid-file: Location '/var/run/mysqld' in the path is accessible to all OS users. Consider choosing a different directory.
mysql_1  | 2021-05-04T16:29:33.770064+09:00 0 [System] [MY-010931] [Server] /usr/sbin/mysqld: ready for connections. Version: '8.0.21'  socket: '/var/run/mysqld/mysqld.sock'  port: 3306  MySQL Community Server - GPL.
go_1     | watching user
go_1     | building...
go_1     | running...
go_1     | API server listening at: [::]:2345
```




## golandでの設定

golandで開発するのがやりやすいと思います。
今はデバッグのサーバしか立ち上がっていないので、ここで設定を行います。
delveのプロセスを走らせることで、APIサーバーも立ち上がるようになります。

メニューの`debug`をクリックし、go remoteを選びます。

![Screen Shot 2021-04-24 at 0.03.36.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/396623/670f4c58-0d69-7a33-400e-ab280dec0a29.png)

適当な名前をつけて、`apply`と`debug`をクリックします。

![Screen Shot 2021-04-24 at 0.03.54.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/396623/a0efb2fc-3f10-ce37-511d-c4119abd06b3.png)

これでコマンドラインでもサーバが立ち上がっています。ただまだマイグレーションができていないので、データベースなどからデータが取得できません。

```
go_1     | API server listening at: [::]:2345
go_1     | [GIN-debug] [WARNING] Creating an Engine instance with the Logger and Recovery middleware already attached.
go_1     | 
go_1     | [GIN-debug] [WARNING] Running in "debug" mode. Switch to "release" mode in production.
go_1     |  - using env:	export GIN_MODE=release
go_1     |  - using code:	gin.SetMode(gin.ReleaseMode)
go_1     | 
go_1     | [GIN-debug] GET    /article                  --> github.com/greenteabiscuit/next-gin-mysql/backend/handler.ArticlesGet.func1 (3 handlers)
go_1     | [GIN-debug] POST   /article                  --> github.com/greenteabiscuit/next-gin-mysql/backend/handler.ArticlePost.func1 (3 handlers)
go_1     | [GIN-debug] Environment variable PORT is undefined. Using port :8080 by default
go_1     | [GIN-debug] Listening and serving HTTP on :8080
```

## マイグレーション

golang-migrateを使ってmysqlデータベースを立ち上げます。`migration`というフォルダを`backend`直下に作ります。

```
backend $ mkdir migration
```

以下のような構造になります。`main.go`でmigrationの処理を書きます。

```
migration $ tree
.
├── main.go
└── migrations
    ├── 1_articles.down.sql
    ├── 1_articles.up.sql
    ├── 2_users.down.sql
    └── 2_users.up.sql
```

### migrationの.env

先ほどと同じになりますが、なるべく見せないようにしましょう。今回はローカルなので大丈夫かと思いますが。

```migration/.env
HTTP_HOST=""
HTTP_PORT=8080
DB_HOST="mysql"
DB_PORT=3306
DB_USERNAME=root
DB_PASSWORD=root
DB_DATABASE=backend
```

### migration の main.go

```migration/main.go
package main

import (
	"database/sql"
	"flag"
	"fmt"
	"os"
	"time"

	_ "github.com/go-sql-driver/mysql"
	"github.com/golang-migrate/migrate"
	"github.com/golang-migrate/migrate/database/mysql"
	_ "github.com/golang-migrate/migrate/source/file"
	"github.com/joho/godotenv"
	"github.com/pkg/errors"
)

var migrationFilePath = "file://./migrations/"

func main() {
	fmt.Println("start migration")
	flag.Parse()
	command := flag.Arg(0)
	migrationFileName := flag.Arg(1)

	if command == "" {
		showUsage()
		os.Exit(1)
	}

	if os.Getenv("USE_HEROKU") != "1" {
		err := godotenv.Load()
		if err != nil {
			fmt.Println(errors.Wrap(err, "load error .env"))
		}
	}

	m := newMigrate()
	version, dirty, _ := m.Version()
	force := flag.Bool("f", false, "force execute fixed sql")
	if dirty && *force {
		fmt.Println("force=true: force execute current version sql")
		m.Force(int(version))
	}

	switch command {
	case "new":
		newMigration(migrationFileName)
	case "up":
		up(m)
	case "down":
		down(m)
	case "drop":
		drop(m)
	case "version":
		showVersionInfo(m.Version())
	default:
		fmt.Println("\nerror: invalid command '", command, "'")
		showUsage()
		os.Exit(0)
	}
}

func generateDsn() string {
	apiRevision := os.Getenv("API_REVISION")
	var dsn string

	if apiRevision == "release" {
		dsn = os.Getenv("DATABASE_URL") + "&multiStatements=true" // heroku対応
	} else {
		user := os.Getenv("DB_USERNAME")
		pass := os.Getenv("DB_PASSWORD")
		host := os.Getenv("DB_HOST")
		port := os.Getenv("DB_PORT")
		dbName := os.Getenv("DB_DATABASE")

		dsn = fmt.Sprintf("%s:%s@tcp(%s:%s)/%s?parseTime=true&multiStatements=true", user, pass, host, port, dbName)
	}

	return dsn
}

func newMigrate() *migrate.Migrate {
	dsn := generateDsn()
	db, openErr := sql.Open("mysql", dsn)
	if openErr != nil {
		fmt.Println(errors.Wrap(openErr, "error occurred. sql.Open()"))
		os.Exit(1)
	}

	driver, instanceErr := mysql.WithInstance(db, &mysql.Config{})
	if instanceErr != nil {
		fmt.Println(errors.Wrap(instanceErr, "error occurred. mysql.WithInstance()"))
		os.Exit(1)
	}

	m, err := migrate.NewWithDatabaseInstance(
		migrationFilePath,
		"mysql",
		driver,
	)

	if err != nil {
		fmt.Println(errors.Wrap(err, "error occurred. migrate.NewWithDatabaseInstance()"))
		os.Exit(1)
	}
	return m
}

func showUsage() {
	fmt.Println(`
-------------------------------------
Usage:
  go run migration/main.go <command>
Commands:
  new FILENAME	Create new up & down migration files
  up		Apply up migrations
  down		Apply down migrations
  drop		Drop everything
  version	Check current migrate version
-------------------------------------`)
}

func newMigration(name string) {
	if name == "" {
		fmt.Println("\nerror: migration file name must be supplied as an argument")
		os.Exit(1)
	}
	base := fmt.Sprintf("./migration/migrations/%s_%s", time.Now().Format("20060102030405"), name)
	ext := ".sql"
	createFile(base + ".up" + ext)
	createFile(base + ".down" + ext)
}

func createFile(fname string) {
	if _, err := os.Create(fname); err != nil {
		panic(err)
	}
}

func up(m *migrate.Migrate) {
	fmt.Println("Before:")
	showVersionInfo(m.Version())
	err := m.Up()
	if err != nil {
		if err.Error() != "no change" {
			panic(err)
		}
		fmt.Println("\nno change")
	} else {
		fmt.Println("\nUpdated:")
		version, dirty, err := m.Version()
		showVersionInfo(version, dirty, err)
	}
}

func down(m *migrate.Migrate) {
	fmt.Println("Before:")
	showVersionInfo(m.Version())
	err := m.Steps(-1)
	if err != nil {
		panic(err)
	} else {
		fmt.Println("\nUpdated:")
		showVersionInfo(m.Version())
	}
}

func drop(m *migrate.Migrate) {
	err := m.Drop()
	if err != nil {
		panic(err)
	} else {
		fmt.Println("Dropped all migrations")
		return
	}
}

func showVersionInfo(version uint, dirty bool, err error) {
	fmt.Println("-------------------")
	fmt.Println("version : ", version)
	fmt.Println("dirty   : ", dirty)
	fmt.Println("error   : ", err)
	fmt.Println("-------------------")
}
```

## マイグレーションのためのsqlファイル

```1_articles.down.sql
drop table if exists articles
```

```1_articles.up.sql
create table if not exists articles (
    id integer auto_increment primary key,
    title varchar(40),
    description varchar(40)
)
```

```2_users.down.sql
drop table if exists users
```

```2_users.up.sql
create table if not exists users (
    id integer auto_increment primary key,
    username varchar(40),
    password varchar(40)
)
```

## マイグレーションする

dockerコンテナに入ってマイグレーションします。`go run main.go up`でマイグレーションが行われます。

```
$ docker exec -it container_name bash
# cd migration
# go run main.go up

start migration
Before:
-------------------
version :  0
dirty   :  false
error   :  no migration
-------------------

Updated:
-------------------
version :  2
dirty   :  false
error   :  <nil>
-------------------
```

これでｄｂでもテーブルができているのを確認したらOKです！
Sequel Proなどのアプリで確認できます。

![Screen Shot 2021-05-04 at 16.38.57.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/396623/3dd048fa-1761-f355-cd2d-38413b2538ac.png)


## PostmanでのAPI確認

これは Postmanというデスクトップアプリで確認することができます。

### Post

`JSON`形式で送ります。

![Screen Shot 2021-04-24 at 0.15.29.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/396623/8c7bc676-f46d-35f1-12a3-ae6f69f4eb86.png)


### GET

![Screen Shot 2021-04-24 at 0.15.40.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/396623/38d30d75-75e1-100f-b567-3fdbdb7a3cfc.png)

データベースにも `hoge` と `hello world`が入っているのが確認できました。

![Screen Shot 2021-05-04 at 16.43.26.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/396623/77205a95-5f32-ea0a-36b0-afa1ac300102.png)

コマンドラインでもカラフルに表示されています。

![Screen Shot 2021-05-04 at 16.59.52.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/396623/b26785c1-7f0e-9a59-ab28-4227fe7f436f.png)



## まとめ

air + gin + gorm + golang-migrate + mysql + delve + dockerで開発用APIサーバーを立ち上げることができました。

## 今後試してみたいこと

- herokuにデプロイしてみる
- userのところの処理がテキトーなままなので直します、、、
- フロントエンド（next + reactなど）とつなげてみる（そもそもレポジトリ名に`next`を含んでいたので繋げようと思ったのですが、記事が長すぎてしまったので次回以降やりたいと思います。）

## 参考

[go-ginでサクッとRESTAPIを構築する](https://qiita.com/shiei_kawa/items/eddf48287455380f618f)

[golang,docker,mysqlの環境をherokuにデプロイする](https://qiita.com/pokotyan/items/51f28eae698c3f7b4d8c)