はじめに
普段Railsを触っていますが、Golangという響きに興味を持ち試しに触ってみました。
慣れない言語だと色々と試しながらで新鮮で楽しくキャッチアップを続けていけそうです。
今回は以前にNext.js, mongoDBを使って作成していたアプリのBE側を「GolangのAPIに置き換える」という名目で進めています。
Golangではじめて書いたコードなのでベストプラクティスではない点はご容赦ください。
これから参考GolangでAPIを書く方の力になれれば幸いです。
何を作るか
冒頭で述べた通り、FEをNext.jsで作成中のBE側をAPIに置き換える名目で作成しています。
このアプリは『Bike Noritai』というアプリでバイカーのツーリングスポットやツーリング記録ができるようなアプリです。
Go言語でREST APIを作ってみる②(User周り)【Echo + GORM + MySQL】
ディレクトリ構成
将来的にはhandler, modelは増やしていく想定です。
大まかな構成はこのようにしています。
.
├── handler
│ ├── spot.go
│ └── user.go
├── model
│ ├── spot.go
│ └── user.go
├── repository
│ └── db.go
├── router
│ └── router.go
├── test
├── .env
│ └── user_test.go
├── .env
├── .gitignore
├── api.dockerfile
├── db.dockerfile
├── docker-compose.yml
├── go.mod
├── go.sum
├── main.go
├── Makefile
├── openapi.yml
└── README.md
OpenAPI
仕様は以下のようなものを想定しています。
FE側で必要なデータはすでに決まっているのでそちらに合わせたレスポンスを返すようにしています。
openapi: "3.0.2"
info:
title: Bike Noritai API
description: |
- Bike Noritai のAPI
- [bike-noritaiフロント](https://github.com/tatsuro1997/bike-noritai)
version: "1.0"
servers:
- url: http://localhost:8080
paths:
"/api/users":
get:
description: ユーザー一覧取得
operationId: UserIndex
responses:
"200":
description: ユーザー一覧
content:
application/json:
schema:
$ref: "#/components/schemas/User"
post:
description: ユーザー登録
operationId: UserCreate
requestBody:
$ref: "#/components/requestBodies/User"
responses:
"201":
description: Created
"/api/users/{user_id}":
get:
description: ユーザー詳細
operationId: UserShow
parameters:
[$ref: "#/components/parameters/UserId"]
responses:
"200":
description: ユーザーデータ
content:
application/json:
schema:
$ref: "#/components/schemas/User"
patch:
description: ユーザー更新
operationId: UserUpdate
parameters:
[$ref: "#/components/parameters/UserId"]
requestBody:
$ref: "#/components/requestBodies/User"
responses:
"204":
description: No Content
delete:
description: ユーザー削除
operationId: UserDelete
parameters:
[$ref: "#/components/parameters/UserId"]
responses:
"204":
description: No Content
"/api/spots":
get:
description: スポット一覧取得
operationId: SpotIndex
responses:
"200":
description: スポットデータ
content:
application/json:
schema:
$ref: "#/components/schemas/Spot"
post:
description: スポット登録
operationId: SpotCreate
requestBody:
content:
application/json:
schema:
$ref: "#/components/schemas/Spot"
responses:
"201":
description: Created
"/api/spots/{spot_id}":
get:
description: スポット詳細取得
operationId: SpotShow
parameters:
[$ref: "#/components/parameters/SpotId"]
responses:
"200":
description: スポットデータ
content:
application/json:
schema:
$ref: "#/components/schemas/Spot"
"/api/users/{user_id}/spots":
get:
description: ユーザー毎スポット一覧取得
operationId: User/SpotIndex
parameters:
[
$ref: "#/components/parameters/UserId",
]
responses:
"200":
description: スポットデータ
content:
application/json:
schema:
$ref: "#/components/schemas/Spot"
"/api/users/{user_id}/spots/{spot_id}":
patch:
description: スポット更新
operationId: SpotUpdate
parameters:
[
$ref: "#/components/parameters/UserId",
$ref: "#/components/parameters/SpotId"
]
requestBody:
$ref: "#/components/requestBodies/Spot"
responses:
"204":
description: No Content
delete:
description: スポット削除
operationId: SpotDelete
parameters:
[
$ref: "#/components/parameters/UserId",
$ref: "#/components/parameters/SpotId"
]
responses:
"204":
description: No Content
"/api/users/{user_id}/records":
get:
description: ツーリング記録一覧取得
operationId: User/RecordIndex
parameters:
[
$ref: "#/components/parameters/UserId",
]
responses:
"200":
description: ツーリング記録データ
content:
application/json:
schema:
$ref: "#/components/schemas/Record"
post:
description: ツーリング記録登録
operationId: RecordCreate
parameters:
[
$ref: "#/components/parameters/UserId",
]
requestBody:
content:
application/json:
schema:
$ref: "#/components/schemas/Spot"
responses:
"201":
description: Created
"/api/users/{user_id}/records/{record_id}":
get:
description: ツーリング記録詳細
operationId: RecordShow
parameters:
[
$ref: "#/components/parameters/UserId",
$ref: "#/components/parameters/RecordId"
]
responses:
"200":
description: ツーリング記録データ
content:
application/json:
schema:
$ref: "#/components/schemas/Record"
patch:
description: ツーリング記録更新
operationId: RecordUpdate
parameters:
[
$ref: "#/components/parameters/UserId",
$ref: "#/components/parameters/RecordId"
]
requestBody:
$ref: "#/components/requestBodies/Record"
responses:
"204":
description: No Content
delete:
description: ツーリング記録削除
operationId: RecordDelete
parameters:
[
$ref: "#/components/parameters/UserId",
$ref: "#/components/parameters/RecordId"
]
responses:
"204":
description: No Content
"/api/users/{user_id}/records/{record_id}/record_likes":
get:
description: ツーリングいいね数
operationId: RecordLikeCount
parameters:
[
$ref: "#/components/parameters/UserId",
$ref: "#/components/parameters/RecordId"
]
responses:
"200":
description: ツーリング記録データ数
content:
application/json:
schema:
type: object
properties:
count:
type: integer
format: int64
"/api/users/{user_id}/comments":
get:
description: ユーザー毎コメント一覧
operationId: CommentIndex
parameters:
[
$ref: "#/components/parameters/UserId",
]
responses:
"200":
description: コメントデータ
content:
application/json:
schema:
$ref: "#/components/schemas/Comment"
"/api/users/{user_id}/records/{record_id}/comments":
get:
description: コメント一覧
operationId: UserCommentIndex
parameters:
[
$ref: "#/components/parameters/UserId",
$ref: "#/components/parameters/RecordId"
]
responses:
"200":
description: コメントデータ
content:
application/json:
schema:
$ref: "#/components/schemas/Comment"
post:
description: コメント作成
operationId: CommentCreate
parameters:
[
$ref: "#/components/parameters/UserId",
$ref: "#/components/parameters/RecordId"
]
requestBody:
$ref: "#/components/requestBodies/Comment"
responses:
"201":
description: Created
patch:
description: コメント更新
operationId: CommentUpdate
parameters:
[
$ref: "#/components/parameters/UserId",
$ref: "#/components/parameters/RecordId"
]
requestBody:
$ref: "#/components/requestBodies/Comment"
responses:
"204":
description: No Content
delete:
description: コメント削除
operationId: CommentDelete
parameters:
[
$ref: "#/components/parameters/UserId",
$ref: "#/components/parameters/RecordId"
]
responses:
"204":
description: No Content
"/api/users/{user_id}/spots/{spot_id}/bookmarks":
get:
description: スポットブックマーク
operationId: BookmarkIndex
parameters:
[
$ref: "#/components/parameters/UserId",
$ref: "#/components/parameters/SpotId"
]
responses:
"200":
description: ブックマークデータ
content:
application/json:
schema:
$ref: "#/components/schemas/Bookmark"
components:
parameters:
UserId:
name: user_id
in: path
required: true
schema:
type: integer
description: ユーザーID
SpotId:
name: spot_id
in: path
required: true
schema:
type: integer
description: スポットID
RecordId:
name: record_id
in: path
required: true
schema:
type: integer
description: レコードID
requestBodies:
User:
content:
application/json:
schema:
type: object
required:
- "name"
- "password"
properties:
email:
type: string
password:
type: string
name:
type: string
area:
type: string
prefecture:
type: string
url:
type: string
bike_name:
type: string
experience:
type: integer
format: int8
Spot:
content:
application/json:
schema:
type: object
required:
- "user_id"
- "name"
properties:
user_id:
type: integer
format: int64
name:
type: string
image:
type: string
type:
type: string
address:
type: string
hp_url:
type: string
open_time:
type: string
off_day:
type: string
parking:
type: boolean
description:
type: string
lat:
type: number
format: float
lng:
type: number
format: float
Record:
content:
application/json:
schema:
type: object
required:
- "user_id"
- "spot_id"
properties:
user_id:
type: integer
format: int64
spot_id:
type: integer
format: int64
date:
type: string
format: date
weather:
type: string
temperature:
type: string
running_time:
type: string
distance:
type: string
description:
type: string
RecordLike:
content:
application/json:
schema:
type: object
required:
- "user_id"
- "spot_id"
properties:
user_id:
type: integer
format: int64
spot_id:
type: integer
format: int64
Comment:
content:
application/json:
schema:
type: object
required:
- "user_id"
- "spot_id"
properties:
user_id:
type: integer
format: int64
spot_id:
type: integer
format: int64
name:
type: string
text:
type: string
Bookmark:
content:
application/json:
schema:
type: object
required:
- "user_id"
- "spot_id"
properties:
user_id:
type: integer
format: int64
spot_id:
type: integer
format: int64
schemas:
User:
type: object
description: ユーザー
properties:
id:
type: integer
format: int64
email:
type: string
password:
type: string
name:
type: string
area:
type: string
prefecture:
type: string
url:
type: string
bike_name:
type: string
experience:
type: integer
created_at:
type: string
format: date-time
updated_at:
type: string
format: date-time
Spot:
type: object
properties:
user_id:
type: integer
format: int64
name:
type: string
image:
type: string
type:
type: string
address:
type: string
hp_url:
type: string
open_time:
type: string
off_day:
type: string
parking:
type: boolean
description:
type: string
lat:
type: number
format: float
lng:
type: number
format: float
created_at:
type: string
format: date-time
updated_at:
type: string
format: date-time
Record:
type: object
properties:
id:
type: integer
format: int64
user_id:
type: integer
format: int64
spot_id:
type: integer
format: int64
date:
type: string
format: date
weather:
type: string
temperature:
type: string
running_time:
type: string
distance:
type: string
description:
type: string
created_at:
type: string
format: date-time
updated_at:
type: string
format: date-time
RecordLike:
type: object
properties:
id:
type: integer
format: int64
user_id:
type: integer
format: int64
spot_id:
type: integer
format: int64
created_at:
type: string
format: date-time
Comment:
type: object
properties:
id:
type: integer
format: int64
user_id:
type: integer
format: int64
spot_id:
type: integer
format: int64
name:
type: string
text:
type: string
created_at:
type: string
format: date-time
Bookmark:
type: object
properties:
id:
type: integer
format: int64
user_id:
type: integer
format: int64
bookmark_id:
type: integer
format: int64
created_at:
type: string
format: date-time
Docker
開発環境はdockerで構築しています。
FROM golang:1.20
WORKDIR /api
COPY go.mod go.sum ./
RUN go mod download && go mod verify
CMD ["go", "run", "main.go"]
FROM mysql:8.0
ENV LANG ja_JP.UTF-8
version: '3.8'
services:
api:
container_name: api
build:
context: .
dockerfile: api.dockerfile
tty: true
ports:
- 8080:8080
depends_on:
- db
volumes:
- .:/api
db:
container_name: db
build:
context: .
dockerfile: db.dockerfile
tty: true
ports:
- 3306:3306
environment:
MYSQL_ROOT_PASSWORD: root
MYSQL_DATABASE: "bike_noritai_dev"
MYSQL_USER: "tester"
MYSQL_PASSWORD: "password"
volumes:
- type: volume
source: mysql_data
target: /var/lib/mysql
networks:
- default
test_db:
container_name: test_db
build:
context: .
dockerfile: db.dockerfile
tty: true
environment:
MYSQL_ROOT_PASSWORD: root
MYSQL_DATABASE: test_db
MYSQL_USER: tester
MYSQL_PASSWORD: password
ports:
- "3307:3306"
volumes:
- type: volume
source: test_mysql_data
target: /var/lib/mysql
networks:
default:
volumes:
mysql_data:
test_mysql_data:
Makefile
開発を進めていく上であると楽なので設定しておきます。
build:
docker-compose build
up:
make down
docker compose up api db
stop:
docker-compose stop
down:
docker-compose down
bash:
docker-compose exec api /bin/bash
db:
docker exec -it db bash
fmt:
docker compose run --rm api gofmt -l -s -w .
go_test:
ENV=test go test -v ./test/...
test_db:
make down
docker compose up test_db
DBの設定
今回はMySQLを使用しています。
他のDBを使用する場合はこちらを参照。
package repository
import (
"log"
"os"
"github.com/joho/godotenv"
"gorm.io/driver/mysql"
"gorm.io/gorm"
)
var (
DB *gorm.DB
err error
)
func init() {
err := godotenv.Load()
if err != nil {
log.Fatal("Error loading .env file")
}
dsn := os.Getenv("DEV_DB_DNS")
if os.Getenv("ENV") == "test" {
dsn = os.Getenv("TEST_DB_DNS")
}
DB, err = gorm.Open(mysql.Open(dsn), &gorm.Config{})
if err != nil {
log.Fatalln(dsn + "database can't connect")
}
環境変数を設定
DEV_DB_DNS="tester:password@tcp(db:3306)/bike_noritai_dev?charset=utf8mb4&parseTime=True&loc=Local"
サーバーの設定
localhost:8080でapiのサーバーを立てるようにしておきます。
package main
import (
rep "bike_noritai_api/repository"
"bike_noritai_api/router"
)
func main() {
db, _ := rep.DB.DB()
defer db.Close()
e := router.NewRouter()
e.Logger.Fatal(e.Start(":8080"))
}
サーバーの疎通確認
ここまで来たらサーバーを起動させ疎通確認をします。
Echoが起動したレスポンスが返ってこればOKです。
$ make build
$ make up
api |
api | ____ __
api | / __/___/ / ___
api | / _// __/ _ \/ _ \
api | /___/\__/_//_/\___/ v4.10.2
api | High performance, minimalist Go web framework
api | https://echo.labstack.com
api | ____________________________________O/_______
api | O\
api | ⇨ http server started on [::]:8080
つづく
今回はGolang + Echo + GROMの開発環境を構築してきました。
次回は、Model、Userハンドラー、routerを追加していきます。
不明点や不備があれば優しく教えていただけると喜びます
参考文献
https://echo.labstack.com/
https://gorm.io/ja_JP/
https://zenn.dev/shimpo/articles/go-echo-gorm-rest-api
https://qiita.com/kiyc/items/c20ac7bb6997c0753314