【Go】GinでREST APIを作ってみる(curlコマンドで動作確認)
はじめに
みんながこぞってgo1.18のジェネリクスしている中、
書いている途中だったこの記事を仕上げます。
Ginの記事はたくさんありますが、Dockerで立てたDBに接続するところまでやっている
記事はあまり見かけなかったので、まとめてみました。
今回はcurlコマンドで動作確認までやっています。
環境
- go 1.17
- github.com/gin-gonic/gin v1.7.7
- github.com/go-sql-driver/mysql v1.4.1
- github.com/go-xorm/xorm v0.7.9
前提
DBはDockerで立てたMySQLを使います。
ORMはXormです。
Ginの概要などは、既にたくさんの記事で紹介されていますし、日本語訳の公式がある為、ここでは書かないです。
Ginについて詳しく知りたい方は公式をご参考ください。
クイックスタート
公式にも記載がありますが、簡単に触れたい場合は、こんな感じで使えます。
main.go
r := gin.Default()
r.GET("/", func(c *gin.Context) {
c.JSON(200, gin.H{
"message":"Hello World",
})
})
r.Run() // 0.0.0.0:8080 でサーバーを立てます。
こちらをmain.goに記載して、go run main.go
でサーバーを立てて、
ブラウザでlocalhost:8080/
を入力すると、{"message":"Hello World"}と表示されます。
ゴール
-
go run main.go
でサーバーを立てて、curlコマンドでCRUDを実行し、Dockerで立てたDBのデータを操作する事ができる。
作ってみた
ソースツリー
こんな感じになりました。
なんちゃってレイヤードアーキテクチャです。
main.goでルーティング→handler層→service層でCRUD処理という感じです。
.
├── README.md
├── docker-compose.yml
├── go.mod
├── go.sum
├── handler
│ └── user.go
├── infra
│ └── xorm.go
├── main.go
├── model
│ └── user.go
└── service
├── service.go
└── user.go
docker-compose.yml
DBはMySQLを使います。
version: "3"
services:
db:
image: mysql:5.7
environment:
- MYSQL_DATABASE=sample_db
- MYSQL_ROOT_PASSWORD=root
command: >
--character-set-server=utf8mb4
--collation-server=utf8mb4_general_ci
ports:
- 3306:3306
main.go
main.goでやっていること。
- 1.infra層でXormを使ってMySQLに接続してます。
- 2.ルーティングで、service層まで処理が走りますが、engineを渡す為に、NewServiceでサービスを別で作成し、engineを渡しています。factoryという名前でサービスを作成してます。
- 3.最後にengineを閉めます。
- 4.Ginは
gin.New
かgin.Default
で初期化出来ます。gin.New
がすっぴんで、gin.Default
はNewにloggerと、recoveryのミドルウェアが入っているものです。 - 5.2で作ったfactoryをginにMiddlewareとして渡します。
- 6.v1というグループを作成してます。グループ毎にmiddlewareとか設定出来るのですが、今回はやっていない為、グループになっている意味はありません。(v1というpathが増えるだけ)
- 7.RESTのルーティングです。
POST:新規作成
,GETALL:全件取得
,GETONE:単体取得
,PUT:更新
,DELETE:削除
の5つを実装してます。 - 8.デフォルトのポートは8080ですが、下記のように3000とか指定出来ます。
func main() {
// 1.engineを作成します
engine := infra.DBInit()
// 2.サービスを作成します
factory := service.NewService(engine)
// 3.最後にengineを閉めます
defer func() {
log.Println("engine closed")
engine.Close()
}()
// 4.Ginを作成します
g := gin.Default()
// 5.サービス層のMiddlewareを作成します
g.Use(service.ServiceFactoryMiddleware(factory))
// 6.v1というグループを作成します
routes := g.Group("/v1")
// 7.ルーティングです
{
routes.POST("/users", handler.Create)
routes.GET("/users", handler.GetAll)
routes.GET("/users/:user-id", handler.GetOne)
routes.PUT("/users/:user-id", handler.Update)
routes.DELETE("/users/:user-id", handler.Delete)
}
// 8.デフォルトは8080です
g.Run(":3000")
}
infra/xorm.go
ここで、MySQLと接続してます。
func DBInit() *xorm.Engine {
engine, err := xorm.NewEngine("mysql", "root:root@tcp([127.0.0.1]:3306)/sample_db?charset=utf8mb4&parseTime=true")
if err != nil {
log.Fatal(err)
}
// xormで使ったSQLをログに吐きます
engine.ShowSQL(true)
// ユーザーテーブルが存在するかチェック
exist, err := engine.IsTableExist("users")
if err != nil {
log.Fatal(err)
}
// 存在しなければテーブルを作成します
if !exist {
engine.CreateTables(&model.Users{})
}
return engine
}
model/user.go
input用の構造体と、DBにinsertする用の構造体を分けています。
構造体の名前が複数形なのは、XormのCreateTableの時に、user構造体だったら、userテーブルという名前になってしまうから...。
なんか方法あるんだろうけど、そこまで調べれていません。
package model
import "time"
type Users struct {
ID int `xorm:"id pk autoincr"`
Name string `xorm:"name"`
Address string `xorm:"address"`
CreatedAt time.Time `xorm:"created_at"`
UpdatedAt time.Time `xorm:"updated_at"`
}
type UserInput struct {
Name string `json:"name" binding:"required" xorm:"name"`
Address string `json:"address" binding:"required" xorm:"address"`
}
service/service.go
ginのMiddlewereにenginを渡す為にコネコネしてます。
もっと簡単に実装出来ると思いますが、一旦これで..。
type Service struct {
engine *xorm.Engine
}
func NewService(engine *xorm.Engine) Servicer {
return &Service{engine}
}
type Servicer interface {
NewUser() *Users
}
func (s *Service) NewUser() *Users {
return NewUsers(s.engine)
}
const (
ServiceKey = "service_factory"
)
func ServiceFactoryMiddleware(svc Servicer) gin.HandlerFunc {
return func(c *gin.Context) {
c.Set(ServiceKey, svc)
c.Next()
}
}
handler/user.go
バインドするには、数種類メソッドがあります。
ShouldBind
とShouldBindWith
の違いは、複数回呼び出せるかどうかです。
ShouldBind
は複数回呼び出せないので、下記のCreateメソッドの中で、2回使用すると、2回目は常にエラーとなります。(https://gin-gonic.com/ja/docs/examples/bind-body-into-dirrerent-structs/)
他にもMustBind
とかもあります。こちらはエラーハンドリング要らないようです。(https://qiita.com/ko-watanabe/items/64134c0a3871856fdc17)
func Create(c *gin.Context) {
input := model.UserInput{}
// ヘッダーのJSONをinputにバインドします
err := c.ShouldBindWith(&input, binding.JSON)
if err != nil {
c.String(http.StatusBadRequest, "Bad request")
return
}
// サービス層をNewします
service := c.MustGet(service.ServiceKey).(service.Servicer)
user := service.NewUser()
// サービス層のCreateメソッドを呼び出します
payload, err := user.Create(&input)
if err != nil {
log.Fatal(err)
}
// ステータス200と、payloadを返します
c.JSON(http.StatusOK, payload)
}
func GetAll(c *gin.Context) {
service := c.MustGet(service.ServiceKey).(service.Servicer)
user := service.NewUser()
payload, err := user.GetAll()
if err != nil {
log.Fatal(err)
}
c.JSON(http.StatusOK, payload)
}
func GetOne(c *gin.Context) {
// user-idのパラメータを数字に変換します
userID, err := strconv.Atoi((c.Param("user-id")))
if err != nil {
log.Fatal(err)
}
service := c.MustGet(service.ServiceKey).(service.Servicer)
user := service.NewUser()
payload, err := user.GetOne(userID)
if err != nil {
log.Fatal(err)
}
c.JSON(http.StatusOK, payload)
}
func Update(c *gin.Context) {
userID, err := strconv.Atoi((c.Param("user-id")))
if err != nil {
log.Fatal(err)
}
input := model.UserInput{}
err = c.ShouldBindWith(&input, binding.JSON)
if err != nil {
c.String(http.StatusBadRequest, "Bad request")
return
}
service := c.MustGet(service.ServiceKey).(service.Servicer)
user := service.NewUser()
payload, err := user.Update(&input, userID)
if err != nil {
log.Fatal(err)
}
c.JSON(http.StatusOK, payload)
}
func Delete(c *gin.Context) {
userID, err := strconv.Atoi((c.Param("user-id")))
if err != nil {
log.Fatal(err)
}
service := c.MustGet(service.ServiceKey).(service.Servicer)
user := service.NewUser()
err = user.Delete(userID)
if err != nil {
log.Fatal(err)
}
c.JSON(http.StatusOK, "削除されました")
}
service/user.go
ここが、DBに直接作用するところです。
基本的なXormでのCRUDになっていると思います。
type Users struct {
engine *xorm.Engine
}
func NewUsers(engine *xorm.Engine) *Users {
u := Users{
engine: engine,
}
return &u
}
func (u *Users) Create(input *model.UserInput) (*model.Users, error) {
now := time.Now()
user := model.Users{
Name: input.Name,
Address: input.Address,
CreatedAt: now,
UpdatedAt: now,
}
_, err := u.engine.Table("users").Insert(&user)
if err != nil {
return nil, err
}
return &user, nil
}
func (u *Users) GetOne(userID int) (*model.Users, error) {
user := model.Users{}
_, err := u.engine.Table("users").Where("id = ?", userID).Get(&user)
if err != nil {
return nil, err
}
return &user, nil
}
func (u *Users) GetAll() ([]*model.Users, error) {
users := []*model.Users{}
err := u.engine.Table("users").Find(&users)
if err != nil {
return nil, err
}
return users, nil
}
func (u *Users) Update(input *model.UserInput, userID int) (*model.Users, error) {
user := model.Users{
Name: input.Name,
Address: input.Address,
}
_, err := u.engine.Table("users").Where("id = ?", userID).Update(&user)
if err != nil {
return nil, err
}
return u.GetOne(userID)
}
func (u *Users) Delete(userID int) error {
user := model.Users{}
_, err := u.engine.Table("users").Where("id = ?", userID).Delete(&user)
if err != nil {
return err
}
return nil
}
curlコマンドで実行してみる。
POST
コマンド
curl -X POST "http://localhost:3000/v1/users" -H "Content-Type: application/json" -d '{"name": "太郎", "address": "大阪府"}'
結果
{"ID":1,"Name":"太郎","Address":"大阪府","CreatedAt":"2022-03-22T00:04:30.927013+09:00","UpdatedAt":"2022-03-22T00:04:30.927013+09:00"}
[xorm] [info] 2022/03/22 00:04:30.927194 [SQL] INSERT INTO `users` (`name`,`address`,`created_at`,`updated_at`) VALUES (?, ?, ?, ?) []interface {}{"太郎", "大阪府", "2022-03-22 00:04:30", "2022-03-22 00:04:30"}
[GIN] 2022/03/22 - 00:04:30 | 200 | 61.976591ms | ::1 | POST "/v1/users"
GET
コマンド
curl -X GET "http://localhost:3000/v1/users/1"
結果
{"ID":1,"Name":"太郎","Address":"大阪府","CreatedAt":"2022-03-22T00:04:30+09:00","UpdatedAt":"2022-03-22T00:04:30+09:00"}
[xorm] [info] 2022/03/22 00:06:27.162554 [SQL] SELECT `id`, `name`, `address`, `created_at`, `updated_at` FROM `users` WHERE (id = ?) LIMIT 1 []interface {}{1}
[GIN] 2022/03/22 - 00:06:27 | 200 | 24.046224ms | ::1 | GET "/v1/users/1"
コマンド
curl -X GET "http://localhost:3000/v1/users"
結果
{"ID":1,"Name":"太郎","Address":"大阪府","CreatedAt":"2022-03-22T00:04:30+09:00","UpdatedAt":"2022-03-22T00:04:30+09:00"}
[GIN-debug] redirecting request 301: /v1/users --> /v1/users
[xorm] [info] 2022/03/22 00:08:56.867604 [SQL] SELECT `id`, `name`, `address`, `created_at`, `updated_at` FROM `users`
[GIN] 2022/03/22 - 00:08:56 | 200 | 10.582069ms | ::1 | GET "/v1/users"
PUT
コマンド
curl -X PUT -H "Content-Type: application/json" -d '{"name": "次郎", "address": "東京都"}' "http://localhost:3000/v1/users/1"
結果
{"ID":1,"Name":"次郎","Address":"東京都","CreatedAt":"2022-03-21T20:11:26+09:00","UpdatedAt":"2022-03-21T20:11:26+09:00"}
[xorm] [info] 2022/03/22 00:11:32.780754 [SQL] UPDATE `users` SET `name` = ?, `address` = ? WHERE (id = ?) []interface {}{"次郎", "東京都", 1}
[xorm] [info] 2022/03/22 00:11:32.791332 [SQL] SELECT `id`, `name`, `address`, `created_at`, `updated_at` FROM `users` WHERE (id = ?) LIMIT 1 []interface {}{1}
[GIN] 2022/03/22 - 00:11:32 | 200 | 20.979005ms | ::1 | PUT "/v1/users/1"
DELETE
コマンド
curl -X DELETE "http://localhost:3000/v1/users/1"
結果
"削除されました"
[xorm] [info] 2022/03/22 00:12:56.459418 [SQL] DELETE FROM `users` WHERE (id = ?) []interface {}{1}
[GIN] 2022/03/22 - 00:12:56 | 200 | 15.044561ms | ::1 | DELETE "/v1/users/1"
さいごに
一通り、curlコマンドで動きました。
次はジェネリクスの波に乗りたいと思います。
改善点あると思いますので、ご指摘頂けたら嬉しいです。
参考
Discussion