🦔
GolangとClean ArchitectureでAPIサーバーを構築してみた
目的
どうも、@yanteraです。
Goで実際にClean Architectureで開発する場合どのような構築をすれば、開発しやすいのか設計してみました。
実践を想定して構築を考えています。APIドキュメントの代わりにsawaggerを実装しています。
使用するツール
- docker
- echo(air)
- gorm
- swagger
- goa
ディレクトリ構成
├─ docker/
├─ api
└─ environments
├─ production/
├─ development/
└─ test/
├─ document
└─ mysql
├─ documents/
└─ design/
├─ server/
├─ adapters/
├─ controllers/
├─ gateways/
└─ presenter/
├─ infrastructure/
├─ routes/
└─ database/
├─ usecases/
├─ entities/
└─ main.go
└─ docker-compose.development.yml
環境について
docker-composeでコンテナを管理しています。コンテナの内容としては以下になります。
- API server
- Clean Architectureを使用してAPIサーバーを構築します
- echo + air + gormを採用
- developmentでは開発効率向上の為にairを採用
- echo + air + gormを採用
- Clean Architectureを使用してAPIサーバーを構築します
- Database
- データの永続化を行います
- MySQL8.0を採用
- データの永続化を行います
- Swagger UI
- SwaggerのUIを構築します
- Swagger Editor
- openapi.yamlを作成・更新します
- goaを採用
- openapi.yamlを作成・更新します
詳細についてはdocker-compose.development.ymlやdockerディレクトリを確認してください。初期データ用のsqlファイルも作成しています。
Clean Architectureについて
今回はクリーンアーキテクチャに沿ってどのようなディレクトリ構成でどのような役割を振っているかのみ説明しています。クリーンアーキテクチャについてはご自身で調べてください。
Adapters
このディレクトリでは外部と通信する為の処理を実装しています。
- Controllers
- Infrastructuresで受けたリクエストを受け取ります。
- リクエストを受け取った後、interactorに処理を投げるための初期化も行います。
- Infrastructuresで受けたリクエストを受け取ります。
search.go
package guest
import (
gateway "api/server/adapters/gateways"
usecase "api/server/usecases/guest"
"github.com/labstack/echo/v4"
"strconv"
)
type GuestController struct {
Interactor usecase.GuestInteractor
}
func InitGuestController(sqlHandler gateway.SQLHandler) *GuestController {
return &GuestController{
Interactor: usecase.GuestInteractor{
GuestPort: &gateway.GuestRepository{
SQLHandler: sqlHandler,
},
},
}
}
func (controller *GuestController) Show(c echo.Context) (err error) {
id, _ := strconv.Atoi(c.Param("id"))
guest, err := controller.Interactor.GuestByID(id)
if err != nil {
c.JSON(500, err)
return
}
c.JSON(200, guest)
return
}
func (controller *GuestController) Index(c echo.Context) (err error) {
guests, err := controller.Interactor.Guests()
if err != nil {
c.JSON(500, err)
return
}
c.JSON(200, guests)
return
- Gateways
- 外部ツールと繋ぐためのinterfaceになります。
- 基本的にはinterfaceの定義とinterfaceを介して外部のメソッドを実行する処理を定義します。
- 外部ツールと繋ぐためのinterfaceになります。
guest_repository.go
package gateways
import (
"api/server/entities"
)
type GuestRepository struct {
SQLHandler
}
func (repo *GuestRepository) FindByID(id int) (guest entities.Guest, err error) {
if err = repo.Find(&guest, id).Error; err != nil {
return
}
return
}
func (repo *GuestRepository) FindAll() (guests entities.Guests, err error) {
if err = repo.Find(&guests).Error; err != nil {
return
}
return
}
sqlhandler.go
package gateways
import (
"gorm.io/gorm"
)
type SQLHandler interface {
First(interface{}, ...interface{}) *gorm.DB
Last(interface{}, ...interface{}) *gorm.DB
Take(interface{}, ...interface{}) *gorm.DB
Find(interface{}, ...interface{}) *gorm.DB
}
- Presenter
- 外部にアウトプットする際のデータをフォーマットします。
- 定義はしていますが今回は未使用です。
- 外部にアウトプットする際のデータをフォーマットします。
Infrastructures
このディレクトリではアプリケーション外部へ通信するための処理を実装します。
- Routes
- ルーティングを行います。
Routesはルーティングが増えると可読性が悪くなるので、ディレクトリを切ってファイル分割出来るようにしています。やり過ぎだと思う方はroute.goだけで良いと思います。
├─ infrastructure/
├─ routes/
└─ guest.go
└─ route.go
route.go
package infrastructure
import (
"api/server/infrastructure/routes"
"github.com/labstack/echo/v4"
"github.com/labstack/echo/v4/middleware"
)
func Run() {
e := echo.New()
e.Use(middleware.Logger())
e.Use(middleware.Recover())
e.Use(middleware.CORS())
routes.InitGuest(e)
e.Logger.Fatal(e.Start(":1323"))
}
guest.go
package routes
import (
controller "api/server/adapters/controllers/guest"
"api/server/infrastructure/database"
"github.com/labstack/echo/v4"
)
func InitGuest(e *echo.Echo) {
guestController := controller.InitGuestController(database.InitSQLHandler())
e.GET("/guests/:id", func(c echo.Context) error { return guestController.Show(c) })
e.GET("/guests", func(c echo.Context) error { return guestController.Index(c) })
}
- Database
- データベースへアクセスするための初期設定を行います。
sql_handler.go
package database
import (
"fmt"
"gorm.io/driver/mysql"
"gorm.io/gorm"
"gorm.io/gorm/logger"
"log"
"os"
"time"
"api/server/adapters/gateways"
)
type SQLHandler struct {
db *gorm.DB
}
func InitSQLHandler() gateways.SQLHandler {
user := os.Getenv("DB_USER")
pass := os.Getenv("DB_PASSWORD")
containerName := os.Getenv("DB_CONTAINER_NAME")
port := os.Getenv("DB_PORT")
dbName := os.Getenv("DB_NAME")
newLogger := logger.New(
log.New(os.Stdout, "\r\n", log.LstdFlags), // io writer
logger.Config{
SlowThreshold: time.Second, // Slow SQL threshold
LogLevel: logger.Info, // Log level
IgnoreRecordNotFoundError: true, // Ignore ErrRecordNotFound error for logger
Colorful: false, // Disable color
},
)
dsn := fmt.Sprintf("%s:%s@tcp(%s:%s)/%s", user, pass, containerName, port, dbName)
db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{
Logger: newLogger,
})
if err != nil {
panic(err)
}
sqlHandler := new(SQLHandler)
sqlHandler.db = db
return sqlHandler
}
func (handler *SQLHandler) First(out interface{}, where ...interface{}) *gorm.DB {
return handler.db.First(out, where...)
}
func (handler *SQLHandler) Take(out interface{}, where ...interface{}) *gorm.DB {
return handler.db.Take(out, where...)
}
func (handler *SQLHandler) Last(out interface{}, where ...interface{}) *gorm.DB {
return handler.db.Last(out, where...)
}
func (handler *SQLHandler) Find(out interface{}, where ...interface{}) *gorm.DB {
return handler.db.Find(out, where...)
}
Usecases
- EntitiesやGatewaysを呼ぶ処理を実装します。
- 個人的には所謂サービスと似たような役割だと思っています。
あえてEntity毎にディレクトリを分けています。すべてのUsecaseを呼ぶのではなく、必要なUsecaseのみ呼べるようにしたいからこのように作成しています。
usecases/
└─ guest
├─ interactor.go
└─ port.go
interactor.go
package guest
import (
"api/server/entities"
)
type GuestInteractor struct {
GuestPort GuestPort
}
func (interactor *GuestInteractor) GuestByID(id int) (guest entities.Guest, err error) {
guest, err = interactor.GuestPort.FindByID(id)
return
}
func (interactor *GuestInteractor) Guests() (guests entities.Guests, err error) {
guests, err = interactor.GuestPort.FindAll()
return
}
port.go
package guest
import (
"api/server/entities"
)
type GuestPort interface {
FindByID(id int) (entities.Guest, error)
FindAll() (entities.Guests, error)
}
Entities
- ドメインロジックを実装します。
- 色んな会社のドメインがあるので一概に言えませんが、強いて言うなら何のデータを作成・更新・読込み・削除をしたいのか考えてください。
guest.go
package entities
type Guests []Guest
type Guest struct {
ID int `json:"id" param:"id"`
FirstName string `json:"first_name"`
FirstNameKana string `json:"first_name_kana"`
LastName string `json:"last_name"`
LastNameKana string `json:"last_name_kana"`
Gender int `json:"gender"`
Birthday string `json:"birthday"`
Tel int `json:"tel"`
Email string `json:"email"`
}
最後に
ここまで読んで下さりありがとうございます。
あくまで今回の実装は考えの一つだと思っています。良いアイディアや改善点があるなと思う方は意見を頂けますと幸いです。
今回の構成はこのページにあるので、興味がある方は是非試してみてください。
Discussion