Gopher道場 Advent Calendar 2018 の 23 日目のエントリです。
昨日は po3rin さんの Go 1.12 beta 1 の標準パッケージの中で気になったマイナーチェンジを Docker で試したので紹介する。 でした!!
TL;DR
- Gin, Gorm で API を作るチュートリアル
- そこそこ管理しやすい構成で作る
Go で開発しやすい API を作りたい
みなさん Go 書いてますか?
昨今 Go は CLI に限らず、 Web の API としても活用される事例が多くなってきています。
今年は Go を勉強して色々試している最中なので、この流れに乗って API を作成してみたいと思います。
というわけで今日は Go, Gin, GORM を使って API を作りましょう!
ただ作ると言っても、ネット上には多くの似たような記事があります。しかし、 main.go
にコードが全部書いてあるものが多く、実際の開発の構成には遠い気がしました。
いくつかの FW を使って API を作ったりネット上のコードを読んだりして、オレオレ構成の構築してみました。
このチュートリアルではエンドポイントが増えても管理しやすいようなディレクトリ構成を目指しました。
リポジトリは以下になります。
Asuforce/gin-gorm-tutorial
API の構成
今日の記事を書くために、いくつかのボイラープレートコードを参考にしました。
ディレクトリ名は Spring Boot っぽくしています。
.
├── controller
│ └── user_controller.go
├── db
│ └── db.go
├── docker-compose.yml
├── entity
│ └── user.go
├── main.go
├── server
│ └── server.go
└── service
└── user_service.go
この構成のポイントは model 層を entity と service に分離しているところです。
model で db のインスタンスを取得、db の初期化時に autoMigration をするために model の構造体が必要という処理が発生するのですが、双方の package を import するため import cycle not allowed
が発生してしまいました。
このエラーを回避するために、 entity
の package を作成し、 model の struct だけを分離するようにしています。
ここに関しては、正直これでいいのかあまり自信がありません、というのも service
, controller
は同一 package 名で entity
は entity package という命名規則が不規則だなと感じるからです。
しかし、今回はこれ以上の解決策を考えられなかったので、コメントなどで指摘していただけると嬉しいです。
あとの主要なディレクトリを紹介すると service
には model の動作を実装し、 server
には gin の設定(ルーティングなど)を実装、controller
はアクションを定義し service
の呼び出しを行うという構成になります。
では下準備としてファイルを作りましょう。
$ mkdir db entity service controller server
$ touch db/db.go entity/user.go service/user_service.go controller/user_controller.go server/server.go
Gin で Hello world
まずは今回使う Gin の Hello, World からです。
$ go get github.com/gin-gonic/gin
package main
import "github.com/gin-gonic/gin"
func main() {
r := gin.Default()
r.GET("/", func(c *gin.Context) {
c.String(200, "Hello, World")
})
r.Run()
}
サーバを起動して、エンドポイントを叩いてみましょう
$ go run main.go
$ curl localhost:8080
Hello, World
メッセージが確認できたら、完了です。
User API の作成
User model を作成し、CRUD 操作ができる API を作成します。
DB の設定の作成
Go の O/RM の GORM を使って DB との接続を行います。
今回は PostgreSQL との接続を行います。他の RDBMS との接続はこちらをご覧ください。
PostgreSQL は Docker で作成しましょう。
version: "3.7"
services:
db:
image: postgres:alpine
environment:
POSTGRES_USER: gorm
POSTGRES_PASSWORD: gorm
POSTGRES_DB: gorm
ports:
- 5432:5432
PostgreSQL を起動しておきましょう。
$ docker-compose up -d
次に gorm と postgres の設定をインストールします。
$ go get github.com/jinzhu/gorm
$ go get github.com/jinzhu/gorm/dialects/postgres
DB の設定はこのようになります。
db のインスタンスは GetDB で取得します。
package db
import (
"github.com/jinzhu/gorm"
_ "github.com/jinzhu/gorm/dialects/postgres" // Use PostgreSQL in gorm
)
var (
db *gorm.DB
err error
)
// Init is initialize db from main function
func Init() {
db, err = gorm.Open("postgres", "host=0.0.0.0 port=5432 user=gorm dbname=gorm password=gorm sslmode=disable")
if err != nil {
panic(err)
}
}
// GetDB is called in models
func GetDB() *gorm.DB {
return db
}
// Close is closing db
func Close() {
if err := db.Close(); err != nil {
panic(err)
}
}
作成した db package を main.go
に import します。(パスは適宜書き換えてください
-import "github.com/gin-gonic/gin"
+import (
+ "github.com/gin-gonic/gin"
+
+ "github.com/asuforce/gin-gorm-tutorial/db"
+)
初期化処理を追加します。
c.String(200, "Hello, World")
})
+ db.Init()
r.Run()
+ db.Close()
以上で db の設定は完了です。
User model の作成
id, FirstName, LastName を持つ User model を作成します。
entity ディレクトリは model の定義だけを扱い、動作に関しては service ディレクトリで管理します。
struct で model を定義します。
package entity
// User is user models property
type User struct {
ID uint `json:"id"`
FirstName string `json:"firstname"`
LastName string `json:"lastname"`
}
Migration の設定
gorm には auto migration 機能が用意されているので、それを使います。
import (
"github.com/jinzhu/gorm"
_ "github.com/jinzhu/gorm/dialects/postgres" // Use PostgreSQL in gorm
+
+ "github.com/asuforce/gin-gorm-tutorial/entity"
)
// ...
if err != nil {
panic(err)
}
+
+ autoMigration()
}
// GetDB is called in models
func GetDB() *gorm.DB {
return db
}
// Close is closing db
func Close() {
if err := db.Close(); err != nil {
panic(err)
}
}
+func autoMigration() {
+ db.AutoMigrate(&entity.User{})
+}
entity を追加するたびに autoMigration に追記することで、テーブルの作成を自動で行うことができます。
以下のコマンドでテーブルが作成されているかを確認できます。
$ go run main.go
$ psql -h 0.0.0.0 -U gorm gorm
Password for user gorm:
psql (11.1)
Type "help" for help.
gorm=# show tables;
ERROR: unrecognized configuration parameter "tables"
gorm=# show tables;\d
ERROR: unrecognized configuration parameter "tables"
List of relations
Schema | Name | Type | Owner
--------+--------------+----------+-------
public | users | table | gorm
public | users_id_seq | sequence | gorm
User service の作成
User model の振るまいを service ディレクトリ以下に定義します。
package user
import (
"github.com/gin-gonic/gin"
"github.com/asuforce/gin-gorm-tutorial/db"
"github.com/asuforce/gin-gorm-tutorial/entity"
)
// Service procides user's behavior
type Service struct{}
// User is alias of entity.User struct
type User entity.User
// GetAll is get all User
func (s Service) GetAll() ([]User, error) {
db := db.GetDB()
var u []User
if err := db.Find(&u).Error; err != nil {
return nil, err
}
return u, nil
}
// CreateModel is create User model
func (s Service) CreateModel(c *gin.Context) (User, error) {
db := db.GetDB()
var u User
if err := c.BindJSON(&u); err != nil {
return u, err
}
if err := db.Create(&u).Error; err != nil {
return u, err
}
return u, nil
}
// GetByID is get a User
func (s Service) GetByID(id string) (User, error) {
db := db.GetDB()
var u User
if err := db.Where("id = ?", id).First(&u).Error; err != nil {
return u, err
}
return u, nil
}
// UpdateByID is update a User
func (s Service) UpdateByID(id string, c *gin.Context) (User, error) {
db := db.GetDB()
var u User
if err := db.Where("id = ?", id).First(&u).Error; err != nil {
return u, err
}
if err := c.BindJSON(&u); err != nil {
return u, err
}
db.Save(&u)
return u, nil
}
// DeleteByID is delete a User
func (s Service) DeleteByID(id string) error {
db := db.GetDB()
var u User
if err := db.Where("id = ?", id).Delete(&u).Error; err != nil {
return err
}
return nil
}
これで User の動作が実装できました。
Controller に action を実装する
Controller にアクションを追加します。
package user
import (
"fmt"
"github.com/gin-gonic/gin"
"github.com/asuforce/gin-gorm-tutorial/service"
)
// Controller is user controlller
type Controller struct{}
// Index action: GET /users
func (pc Controller) Index(c *gin.Context) {
var s user.Service
p, err := s.GetAll()
if err != nil {
c.AbortWithStatus(404)
fmt.Println(err)
} else {
c.JSON(200, p)
}
}
// Create action: POST /users
func (pc Controller) Create(c *gin.Context) {
var s user.Service
p, err := s.CreateModel(c)
if err != nil {
c.AbortWithStatus(400)
fmt.Println(err)
} else {
c.JSON(201, p)
}
}
// Show action: GET /users/:id
func (pc Controller) Show(c *gin.Context) {
id := c.Params.ByName("id")
var s user.Service
p, err := s.GetByID(id)
if err != nil {
c.AbortWithStatus(404)
fmt.Println(err)
} else {
c.JSON(200, p)
}
}
// Update action: PUT /users/:id
func (pc Controller) Update(c *gin.Context) {
id := c.Params.ByName("id")
var s user.Service
p, err := s.UpdateByID(id, c)
if err != nil {
c.AbortWithStatus(400)
fmt.Println(err)
} else {
c.JSON(200, p)
}
}
// Delete action: DELETE /users/:id
func (pc Controller) Delete(c *gin.Context) {
id := c.Params.ByName("id")
var s user.Service
if err := s.DeleteByID(id); err != nil {
c.AbortWithStatus(403)
fmt.Println(err)
} else {
c.JSON(204, gin.H{"id #" + id: "deleted"})
}
}
Routing を設定する
server ディレクトリを作成し、ルーティングを設定します。
$ mkdir server
routing の実装はこのようになります。
package server
import (
"github.com/gin-gonic/gin"
"github.com/asuforce/gin-gorm-tutorial/controller"
)
// Init is initialize server
func Init() {
r := router()
r.Run()
}
func router() *gin.Engine {
r := gin.Default()
u := r.Group("/users")
{
ctrl := user.Controller{}
u.GET("", ctrl.Index)
u.GET("/:id", ctrl.Show)
u.POST("", ctrl.Create)
u.PUT("/:id", ctrl.Update)
u.DELETE("/:id", ctrl.Delete)
}
return r
}
最後に main.go
を修正します。
import (
- "github.com/gin-gonic/gin"
-
"github.com/asuforce/gin-gorm-tutorial/db"
+ "github.com/asuforce/gin-gorm-tutorial/server"
)
func main() {
- r := gin.Default()
- r.GET("/", func(c *gin.Context) {
- c.String(200, "Hello, World")
- })
-
db.Init()
- r.Run()
+ server.Init()
}
error handling や validation など、できてない部分も多々ありますが、以上で API の実装は完了です。
curl で試す
サーバを起動して、以下のような挙動を確認できれば OK です。
$ go run main.go
# Index
$ curl http://localhost:8080/users -X GET
[]%
# Create
$ curl http://localhost:8080/users -X POST -H "Content-Type: application/json" -d '{"FirstName": "Rikka", "LastName": "Takarada"}'
{"id":1,"firstname":"Rikka","lastname":"Takarada"}
# Update
$ curl http://localhost:8080/users/1 -X PUT -H "Content-Type: application/json" -d '{"FirstName": "Akane", "LastName": "Shinjo"}'
{"id":1,"firstname":"Akane","lastname":"Shinjo"}
# Show
$ curl http://localhost:8080/users/1 -X GET
{"id":1,"firstname":"Akane","lastname":"Shinjo"}
# Delete
$ curl http://localhost:8080/users/1 -X DELETE
まとめ
そこそこ管理しやすい構成の API が完成したと思います。
他のモデルを追加するなどして、遊んでみてください。
今回、この API を作ってみて面白かったのは、go のフレームワークには特にこれだと決まった構成はないんだなという発見でした。
自分自身で、最適なディレクトリ構成が組み立てられるのは他の言語では体験したことがない感覚だったので新鮮でした。
MVC だけでなく他のアーキテクチャも自身で設計、構成まで自由に作り込めそうだなと感じました。
今後も Go の API を研究して、自分やチームにベストな構成を編み出していければと思います。
もっと「こういう構成があるよ」などの情報をお待ちしております。
最後まで読んでいただきありがとうございました!