Goにおける「俺的」クリーンアーキテクチャ構成
はじめに
こんにちは、mizukoです!
親の顔より見たGoのクリーンアーキテクチャ構成について、自分はこうしているよ!というのをアウトプットしていきます!
Goのディレクトリ構成に悩んでいる人の参考になりましたら幸いです!
構成
まず全体像です。
.
├── .devcontainer
├── .env
├── .gitignore
├── Makefile
├── compose.yml
├── config
│ ├── config.go
├── docker
│ ├── golang
│ │ └── Dockerfile
│ └── postgres
│ ├── Dockerfile
│ └── pg_hba.conf
├── domain
│ ├── model
│ │ ├── category.go
│ │ ├── memo.go
│ │ └── user.go
│ └── repository
│ ├── category
│ │ └── category.go
│ ├── common
│ │ └── transaction.go
│ ├── memo
│ │ └── memo.go
│ └── user
│ └── user.go
├── go.mod
├── go.sum
├── go.work
├── go.work.sum
├── infrastructure
│ ├── auth
│ │ └── jwt
│ │ └── jwt.go
│ ├── database
│ │ └── postgres
│ │ └── bun
│ │ ├── connection.go
│ │ ├── mapper
│ │ │ ├── category.go
│ │ │ ├── memo.go
│ │ │ └── user.go
│ │ ├── migrate
│ │ │ ├── generate.go
│ │ │ ├── migrations
│ │ │ └── schema.sql
│ │ ├── model
│ │ │ ├── category.go
│ │ │ ├── memo.go
│ │ │ └── user.go
│ │ ├── query
│ │ │ └── query.go
│ │ ├── repository
│ │ │ ├── category
│ │ │ ├── common
│ │ │ ├── memo
│ │ │ └── user
│ │ └── seeds
│ │ └── main.go
│ └── external
│ └── google
│ └── auth.go
├── main.go
├── mocks
├── presentation
│ └── http
│ ├── constants
│ │ └── xxx.go
│ ├── handler
│ │ ├── auth
│ │ │ ├── auth.go
│ │ ├── category
│ │ │ └── category.go
│ │ ├── memo
│ │ │ └── memo.go
│ │ └── user
│ │ └── user.go
│ ├── helper
│ │ ├── context_helper.go
│ │ └── error_handler.go
│ ├── middleware
│ │ └── authentication.go
│ ├── request
│ │ ├── auth
│ │ │ ├── inputs.go
│ │ │ └── validator.go
│ │ ├── helper.go
│ │ ├── memo
│ │ │ ├── inputs.go
│ │ │ └── validator.go
│ │ └── user
│ │ ├── inputs.go
│ │ └── validator.go
│ ├── response
│ │ ├── auth
│ │ │ └── auth_response.go
│ │ ├── memo
│ │ │ └── get_user_memo_response.go
│ │ └── user
│ │ └── user_response.go
│ └── router
│ └── router.go
├── registry
│ └── registry.go
└── usecase
├── category
│ └── category.go
├── memo
│ ├── memo.go
│ └── memo_test.go // 例
└── user
└── user.go
以下についてそれぞれ説明していきたいと思います。
- domain
- usecase
- presentation
- infrastructure
- registry
- main.go
domain
クリーンアーキテクチャにおける「Entities」部分で、ドメインロジックを集約するディレクトリです。
modelにはentityを、repositoryにはインターフェースを定義します。
modelのディレクトリ名はentityでも良いかと思いますが、そこはお好みで。
package model
import (
"time"
)
type Category struct {
ID uint `json:"id"`
Name string `json:"name"`
CreatedAt time.Time `json:"-"`
UpdatedAt time.Time `json:"-"`
Memos []Memo `json:"memos"`
}
// ドメインに関わる処理がある場合は以下の様に定義していく(めっちゃ例です)
func (c *Category) SetCreatedAt() {
c.CreatedAt = time.Now()
}
package category
import (
".../domain/model"
)
type CategoryRepository interface {
FindByID(id uint) (*model.Category, error)
FindByIDs(ids []uint) ([]model.Category, error)
GetList() ([]model.Category, error)
Store(category model.Category) (*model.Category, error)
}
usecase
クリーンアーキテクチャにおける「Use Cases」部分で、アプリケーション固有のビジネスロジックを集約するディレクトリです。
現在はusecase配下に各サブドメインが並んでいますが、サブドメインを跨いで共通の処理がある場合などに備えて、以下構成にするのもありかと思います。
.
├── interactors
│ ├── category
│ └── user
└── services
また、サブドメイン配下のファイルはcategory.goとしていますが、1ユースケース = 1ファイルとする構成もありかと思いますし、いくつかのまとまり(read、write、searchなど)でファイルを切るのも良いかと思います。
プロジェクトの規模に応じて良い感じに分けていきましょう。
現在私の場合は、以下の様にインターフェースも実装も一つのファイルで管理しています。
usecaseのロジックが肥大化してくる様なら上記戦略も考えようかなぁ〜という温度感です。
最初からきっちり疎結合にしたい!という方は、1ユースケース1ファイルがオススメです。
package category
import (
".../domain/model"
domainRepository ".../domain/repository/category"
)
type CategoryUsecaseImpl struct {
CategoryRepository domainRepository.CategoryRepository
}
func NewCategoryUsecase(repo domainRepository.CategoryRepository) CategoryUsecase {
return &CategoryUsecaseImpl{
CategoryRepository: repo,
}
}
type CategoryUsecase interface {
GetList() ([]model.Category, error)
}
func (i *CategoryUsecaseImpl) GetList() ([]model.Category, error) {
return i.CategoryRepository.GetList()
}
presentation
クリーンアーキテクチャにおける「Interface Adapters」部分で、外部からのデータのやり取りやユースケースの呼び出しなどを行います。
presentation配下に現状httpしかありませんが、gRPCやcliなどの追加を想定しています。
http配下については、主要なhandlerとrouterを紹介します。
middlewareやhelperなどは、別途記事にできたらなぁ〜と思ってます!
handler
handlerは、外部からのデータの変換やユースケースの呼び出しなど、インターフェース・アダプター層の中核を担います。
webフレームワークはGinを使っています。
package memo
import (
"net/http"
"strconv"
".../presentation/http/constants"
".../presentation/http/helper"
request ".../presentation/http/request/memo"
response ".../presentation/http/response/memo"
usecase ".../usecase/memo"
"github.com/gin-gonic/gin"
)
type MemoHandler struct {
MemoUseCase usecase.MemoUsecase
Validator *request.MemoValidator
}
func NewMemoHandler(memoUseCase usecase.MemoUsecase) MemoHandler {
return MemoHandler{
MemoUseCase: memoUseCase,
Validator: request.NewMemoValidator(),
}
}
~ 略 ~
func (h *MemoHandler) Create(c *gin.Context) {
contextUser := helper.GetUserFromContext(c)
var input request.CreateMemoRequest
if err := c.Bind(&input); err != nil {
helper.HandleInternalServerError(c, err.Error())
return
}
if err := h.Validator.ValidateCreateMemoRequest(input); err != nil {
helper.HandleValidationErrors(c, err, h.Validator.Translator)
return
}
_, err := h.MemoUseCase.Create(contextUser.ID, input)
if err != nil {
helper.HandleBadRequest(c, err.Error())
return
}
c.JSON(http.StatusCreated, gin.H{})
}
~ 略 ~
router
routerはAPIのパスを定義します。
middlewareやcorsの設定などもここで行います。
package router
import (
"os"
"time"
".../presentation/http/handler/auth"
".../presentation/http/handler/category"
".../presentation/http/handler/memo"
".../presentation/http/middleware"
"github.com/gin-contrib/cors"
"github.com/gin-gonic/gin"
)
type Router struct {
AuthHandler auth.AuthHandler
CategoryHandler category.CategoryHandler
MemoHandler memo.MemoHandler
AuthMiddleware middleware.AuthMiddleware
}
func NewRouter(
authHandler auth.AuthHandler,
categoryHandler category.CategoryHandler,
memoHandler memo.MemoHandler,
authMiddleware middleware.AuthMiddleware,
) Router {
return Router{
AuthHandler: authHandler,
CategoryHandler: categoryHandler,
MemoHandler: memoHandler,
AuthMiddleware: authMiddleware,
}
}
func CORSConfig() cors.Config {
return cors.Config{
AllowOrigins: []string{
os.Getenv("APP_URL"),
},
AllowMethods: []string{
"POST",
"GET",
"PUT",
"DELETE",
"OPTIONS",
},
AllowHeaders: []string{
"Access-Control-Allow-Credentials",
"Access-Control-Allow-Headers",
"Content-Type",
"Content-Length",
"Accept-Encoding",
"Authorization",
},
AllowCredentials: true,
MaxAge: 1 * time.Hour,
}
}
func (r *Router) SetupRoutes(e *gin.Engine) {
// CORSの設定
e.Use(cors.New(CORSConfig()))
v1Group := e.Group("/v1")
v1Group.POST("/auth/login", r.AuthHandler.Login)
v1Group.GET("/auth/verify", r.AuthHandler.Verify)
v1Group.GET("/auth/refresh", r.AuthHandler.Refresh)
~ 略 ~
v1Group.GET("/memos", r.MemoHandler.GetList)
// 認証が必要なルートグループ
authorizedGroup := v1Group.Group("/me")
authorizedGroup.Use(r.AuthMiddleware.Authentication())
{
~ 略 ~
authorizedGroup.GET("/memos", r.MemoHandler.GetListByUser)
authorizedGroup.POST("/memo", r.MemoHandler.Create)
~ 略 ~
}
}
infrastructure
クリーンアーキテクチャにおける「Frameworks & Drivers」部分で、データベース、外部APIサービスなどの外部リソースとの連携を行います。
authとexternalについては割愛しますが、名前の通り、それぞれ認証に関する役割と外部サービスとの連携を担っています。
database
その名の通り、databaseに関わる処理を集約します。
database配下に現在はpostgresしかありませんが、MySQLやSQLite等、別のDBクライアントが並びます。
さらにpostgres配下には現状bunしかありませんが、Gorm等別のORMが並びます。
この構成にすることにより、別のDBクライアントやORMに簡単に切り替えることができます。(私も実際GormからBunへリプレイスしましたが、ORM部分だけ切り替えるだけでリプレイスが完了しました。)
repository
domainで定義したrepositoryの実態を実装します。
package category
import (
"context"
"database/sql"
"errors"
"log"
domainModel ".../domain/model"
domainRepository ".../domain/repository/category"
".../infrastructure/database/postgres/bun/mapper"
ormModel ".../infrastructure/database/postgres/bun/model"
"github.com/uptrace/bun"
)
type categoryRepository struct {
db *bun.DB
}
func NewCategoryRepository(db *bun.DB) domainRepository.CategoryRepository {
return &categoryRepository{db: db}
}
func (r *categoryRepository) FindByID(id uint) (*domainModel.Category, error) {
var category ormModel.Category
err := r.db.NewSelect().Model(&category).Where("id = ?", id).Scan(context.TODO())
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
return nil, nil
}
log.Printf("Could not query category: %v", err)
return nil, err
}
return mapper.ToDomainCategoryModel(&category), nil
}
mapper
domainモデルとORMのモデルとのマッピング担います。
mapperを挟むことにより、domainモデルがデーターベース側に依存しなくなるので、先述した通り、DBやORMの切り替えが容易になります。
package mapper
import (
domainModel ".../domain/model"
ormModel ".../infrastructure/database/postgres/bun/model"
)
func ToDomainCategoryModel(ormCategory *ormModel.Category) *domainModel.Category {
domainMemos := make([]domainModel.Memo, len(ormCategory.Memos))
for i, ormMemo := range ormCategory.Memos {
domainMemos[i] = *ToDomainMemoModel(&ormMemo)
}
return &domainModel.Category{
ID: ormCategory.ID,
Name: ormCategory.Name,
CreatedAt: ormCategory.CreatedAt,
UpdatedAt: ormCategory.UpdatedAt,
Memos: domainMemos,
}
}
func ToORMCategoryModel(domainCategory *domainModel.Category) *ormModel.Category {
ormMemos := make([]ormModel.Memo, len(domainCategory.Memos))
for i, domainMemo := range domainCategory.Memos {
ormMemos[i] = *ToORMMemoModel(&domainMemo)
}
return &ormModel.Category{
ID: domainCategory.ID,
Name: domainCategory.Name,
CreatedAt: domainCategory.CreatedAt,
UpdatedAt: domainCategory.UpdatedAt,
Memos: ormMemos,
}
}
model
ORMのモデルです。
bunの場合は以下の様な感じでタグを付与しています。
package model
import (
"time"
"github.com/uptrace/bun"
)
type Category struct {
bun.BaseModel `bun:"table:categories,alias:c"`
ID uint `bun:",pk,autoincrement"`
Name string `bun:",type:varchar(100)"`
CreatedAt time.Time `bun:",notnull,default:current_timestamp"`
UpdatedAt time.Time `bun:",notnull,default:current_timestamp"`
Memos []Memo `bun:"m2m:memo_categories,join:Category=Memo"`
}
registry
自作のDIコンテナを定義します。
個人的にはライブラリを使うまでも無く、registryを見れば依存関係を一目瞭然できる今の構成が気に入っています。
~ 略 ~
type Registry struct {
GoogleAuthService googleAuthService.GoogleAuthService
AuthHandler authHandler.AuthHandler
GoogleAuthHandler authHandler.GoogleAuthHandler
UserRepository *userDomainRepository.UserRepository
UserUsecase userUsecase.UserUsecase
UserHandler userHandler.UserHandler
MemoRepository *memoDomainRepository.MemoRepository
MemoUsecase memoUsecase.MemoUsecase
MemoHandler memoHandler.MemoHandler
CategoryRepository *categoryDomainRepository.CategoryRepository
CategoryUsecase categoryUsecase.CategoryUsecase
CategoryHandler categoryHandler.CategoryHandler
AuthMiddleware middleware.AuthMiddleware
TxRepository *commonDomainRepository.TxRepository
Router router.Router
}
func NewRegistry(db *bun.DB) *Registry {
r := &Registry{}
r.initRegistry(db)
return r
}
func (r *Registry) initRegistry(db *bun.DB) {
txRepository := commonInfraRepository.NewTxRepository(db)
r.TxRepository = &txRepository
googleAuthService := googleAuthService.NewGoogleAuthService()
r.GoogleAuthService = googleAuthService
userRepository := userInfraRepository.NewUserRepository(db)
userUsecase := userUsecase.NewUserUsecase(userRepository)
r.UserRepository = &userRepository
r.UserUsecase = userUsecase
r.AuthHandler = authHandler.NewAuthHandler(userUsecase)
r.GoogleAuthHandler = authHandler.NewGoogleAuthHandler(userUsecase, googleAuthService)
r.UserHandler = userHandler.NewUserHandler(userUsecase)
r.AuthMiddleware = middleware.NewAuthMiddleware(userRepository)
categoryRepository := categoryInfraRepository.NewCategoryRepository(db)
categoryUsecase := categoryUsecase.NewCategoryUsecase(categoryRepository)
r.CategoryHandler = categoryHandler.NewCategoryHandler(categoryUsecase)
r.CategoryRepository = &categoryRepository
r.CategoryUsecase = categoryUsecase
memoRepository := memoInfraRepository.NewMemoRepository(db)
memoUsecase := memoUsecase.NewMemoUsecase(memoRepository, categoryRepository, txRepository)
r.MemoRepository = &memoRepository
r.MemoUsecase = memoUsecase
r.MemoHandler = memoHandler.NewMemoHandler(memoUsecase)
r.Router = router.NewRouter(
r.AuthHandler,
r.GoogleAuthHandler,
r.CategoryHandler,
r.MemoHandler,
r.UserHandler,
r.AuthMiddleware,
)
}
main.go
データベースの接続やDIコンテナの初期化、ルーティングのセットアップなどを行います。
package main
import (
"log"
postgres ".../infrastructure/database/postgres/bun"
".../registry"
"github.com/gin-gonic/gin"
)
func main() {
// データベース接続を作成
db, err := postgres.LoadConfigAndCreateDBConnection()
if err != nil {
log.Fatalf("Could not connect to database: %v", err)
}
// DIコンテナを初期化
registry := registry.NewRegistry(db)
// ルーティングの設定
r := gin.Default()
registry.Router.SetupRoutes(r)
r.Run(":8080")
}
まとめ
ということで、Goでやる俺的クリーンアーキテクチャ構成を殴り書きしてみました!
雰囲気は伝わったでしょうか...?
あくまで「現状」なので、また明日には変わっているかもしれませんが、都度更新できたらなぁと思ってます。(たぶん)
また、middlewareやdtoなど、今回書ききれなかったことを別途記載する予定ですので、是非フォローやいいねお願いします🫶
Discussion