😺

GinでAPI作ってみる

2023/04/15に公開

背景

この前はechoでAPI作ったので、今度はGinを使ってみる。
とりあえずは何か一つ動くものを作成する。
クリーンアキテクチャーも意識したフォルダ構成にする。

環境

M1 Mac
docker desktop 4.17.0

全体感

依存関係

クリーンアーキテクチャーを意識して、依存関係は図のようにする。
usecaseとかもあった方がよいかもしれないが、現時点だとちょっとやりすぎなかんじもするので、やらない。

フォルダ構成

最終的なフォルダ・ファイル構成はこんなかんじ。

├── Dockerfile
├── README.md
├── config
│   └── config.go
├── db
│   └── mysql
│       ├── etc
│       │   └── my.cnf
│       ├── init
│       │   └── init.sql
│       └── log
├── docker-compose.yml
├── domain
│   ├── model
│   │   └── user.go
│   ├── repository
│   │   └── user_repository.go
│   └── service
│       └── user_service.go
├── dto
│   ├── input
│   │   └── user_input_dto.go
│   └── output
│       └── user_output_dto.go
├── go.mod
├── go.sum
├── handler
│   └── user_handler.go
├── infrastructure
│   └── mysql
│       ├── mysql.go
│       └── user_repository.go
├── main.go

実装

準備

・初期化

go mod init go-gin-api

Ginをインストール

go get -u github.com/gin-gonic/gin

GORMをインストール

go get -u gorm.io/gorm
go get github.com/go-sql-driver/mysql

メイン実装

各層はインタフェース経由で呼び出すようにして、疎結合にしておく。
(すべてのコードを記載すると量が多くなるので、主要なところだけ載せておきます。)

infrastructure

mysql.go
Mysqlとのconnection作るところ

package mysql

import (
	"log"
	"time"

	"gorm.io/driver/mysql"
	"gorm.io/gorm"
)

var DB *gorm.DB

func Connect(host string, user string, password string, port string, databaseName string) {
	var err error
	dsn := user + ":" + password + "@tcp(" + host + ":" + port + ")/" + databaseName + "?parseTime=true" + "&charset=utf8mb4"
	for i := 0; i < 5; i++ {
		DB, err = gorm.Open(mysql.Open(dsn), &gorm.Config{})

		if err == nil {
			break
		}
		time.Sleep(time.Millisecond * 100)
	}
	if err != nil {
		log.Printf("Mysql接続に失敗。エラー内容:%s", err.Error())
	} else {
		log.Print("Mysql接続に成功")
	}
}

user_repository.go
Mysqlに接続して実際にデータ取得するところ

package mysql

import (
	"database/sql"
	"errors"
	"go-gin-api/domain/model"
	"go-gin-api/domain/repository"

	"gorm.io/gorm"
)

type UserRepository struct {
	*sql.DB
}

func NewUserRepository(db *sql.DB) repository.IfUserRepository {
	return &UserRepository{db}
}

func (ur *UserRepository) FindByID(id int) (*model.User, error) {
	u := &model.User{}
	err := DB.First(&u, id).Error
	if errors.Is(err, gorm.ErrRecordNotFound) {
		return u, nil
	}
	if err != nil {
		return u, err
	}
	return u, nil
}



service

user_repository.go
infraとの仲介をするインタフェースのみ定義しておく

package repository

import "go-gin-api/domain/model"

type IfUserRepository interface {
	FindByID(id int) (*model.User, error)
}

user_service.go
repositoryの処理の呼び出し

package service

import (
	"go-gin-api/domain/model"
	"go-gin-api/domain/repository"
	"go-gin-api/dto/input"
	"go-gin-api/dto/output"
)

type IfUserService interface {
	FindByID(id int) (*output.User, error)
}

type UserService struct {
	repository.IfUserRepository
}

func NewUserService(repo repository.IfUserRepository) IfUserService {
	return &UserService{repo}
}

func (s *UserService) convertTo(user *model.User) *output.User {
	return output.NewUserModel(user.ID, user.UserName, user.UpdatedAt, user.CreatedAt, user.DeletedAt)
}

func (s *UserService) convertFrom(user *input.User) *model.User {
	return model.NewUser(user.ID, user.UserName, user.UpdatedAt, user.CreatedAt, user.DeletedAt)
}

func (s *UserService) FindByID(id int) (*output.User, error) {
	user, err := s.IfUserRepository.FindByID(id)
	if err != nil {
		return nil, err
	}
	return s.convertTo(user), nil
}

handler

user_handler.go
ginからリクエストデータ取得してservice呼び出し

package handler

import (
	"go-gin-api/domain/service"
	"net/http"
	"strconv"

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

type IfUserHandler interface {
	FindByID(c *gin.Context)
}

type UserHandler struct {
	service.IfUserService
}

func NewUserHandler(s service.IfUserService) IfUserHandler {
	return &UserHandler{s}
}

func (uh UserHandler) FindByID(c *gin.Context) {
	id, _ := strconv.Atoi(c.Param("id"))
	user, err := uh.IfUserService.FindByID(id)
	if err != nil {
		c.JSON(http.StatusNotFound, gin.H{
			"msg": "エラーが発生しました。",
		})
	}

	c.JSON(http.StatusOK, gin.H{
		"id":         user.ID,
		"userName":   user.UserName,
		"updated_at": user.UpdatedAt,
		"created_at": user.CreatedAt,
		"deleted_at": user.DeletedAt,
	})

}

エントリポイント

main.go
ここでDIする。
ミドルウェアの設定も入れておく。

package main

import (
	"go-gin-api/config"
	"go-gin-api/domain/service"
	"go-gin-api/handler"
	"go-gin-api/infrastructure/mysql"
	"net/http"

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

func main() {
	r := gin.Default()

	r.Use(cors.New(cors.Config{
		AllowOrigins: []string{"*"},
		AllowMethods: []string{
			"POST",
			"GET",
			"PUT",
			"DELETE",
		},
		AllowHeaders: []string{
			"Access-Control-Allow-Credentials",
			"Access-Control-Allow-Headers",
			"Content-Type",
			"Content-Length",
			"Accept-Encoding",
			"Authorization",
		},
	}))

	mysql.Connect(config.Config.MysqlHost, config.Config.MysqlUser, config.Config.MysqlPass, config.Config.MysqlPort, config.Config.MysqlDbName)
	db, _ := mysql.DB.DB()
	defer db.Close()

	r.GET("/", func(c *gin.Context) {
		c.JSON(http.StatusOK, gin.H{"message": "Hello, World!"})
	})

	r.GET("/user/:id", func(c *gin.Context) {
		r := mysql.NewUserRepository(db)
		s := service.NewUserService(r)
		h := handler.NewUserHandler(s)
		h.FindByID(c)
	})

	r.Run(":" + config.Config.ServerPort)
}

動作確認

コンテナ起動
ここでMysqlに初期データを投入

docker-compose up -d

API実行。
レスポンスくることを確認。

Discussion