LoginSignup
7
7

More than 5 years have passed since last update.

Go言語のRESTサーバーを Clean Architecture で作ってみる

Last updated at Posted at 2019-03-26

今回は Go 言語で、以前 Kotlin で書いたサーバーと同程度の物を作ろうと思います。

それと、Clean Architecture の記事も書いたので、それらしいレイヤー構成にしてみます。

Data Layer

Entity の定義です。
JSONのKey名を指定したい場合は バッククォート で定義します。
また、テーブル名は標準だと Entity の複数形になるので、別名にする場合は TableName メソッドを用意します。

user_entity.go
package entity

type User struct {
    ID      int64        `json:"-"`
    Name    data.Name    `json:"名前"`
    Age     data.Age     `json:"歳"`
    Address data.Address `json:"住所"`
}

func (User) TableName() string {
    return "user"
}

Repository としてはI/Fを定義するだけです。

user_repository.go
package repository

type UserRepository interface {
    Get([]string) (*[]entity.User, error)
    GetAll() (*[]entity.User, error)
    Delete(string) (*entity.User, error)
    Create(*entity.User) (*entity.User, error)
}

var User UserRepository

GORM に依存した形の Repository の実装です。

user_db.go
package db

type UserRepositoryImpl struct {
    DB *gorm.DB
}

func (r *UserRepositoryImpl) Get(ids []string) (*[]entity.User, error) {
    users := []entity.User{}
    err := r.DB.Where(ids).Find(&users).Error
    return &users, err
}

func (r *UserRepositoryImpl) GetAll() (*[]entity.User, error) {
    users := []entity.User{}
    err := r.DB.Find(&users).Error
    return &users, err
}

func (r *UserRepositoryImpl) Delete(id string) (*entity.User, error) {
    user := entity.User{}
    err := r.DB.Where("id = ?", id).Delete(&user).Error
    return &user, err
}

func (r *UserRepositoryImpl) Create(user *entity.User) (*entity.User, error) {
    err := r.DB.Create(user).Error
    return user, err
}

Domain Layer

UseCase の実装です。
本来なら Model を定義して変換した方がよいのですが、今回は端折りました。

user_usecase.go
package usecase

func GetUsers() (*[]entity.User, error) {
    return repository.User.GetAll()
}

func GetUser(id string) (*entity.User, error) {
    users, err := repository.User.Get([]string{id})
    if len(*users) == 0 {
        return nil, errors.New("record not found")
    }
    return &(*users)[0], err
}

func CreateUser(user *entity.User) (*entity.User, error) {
    return repository.User.Create(user)
}

func DeleteUser(id string) (*entity.User, error) {
    return repository.User.Delete(id)
}

Presentation Layer

web_api.go
package presentation

var Api WebApi

type WebApi interface {
    setup()
}

func Setup() {
    Api.setup()
}
gin_api.go
package presentation


type GinApi struct {
    Address string
}

func (p *GinApi) setup() {
    router := gin.Default()
    router.Use(limit.MaxAllowed(100))

    user := router.Group("/user")
    {
        user.GET("/", func(ctx *gin.Context) {
            framework(ctx, func() (interface{}, error) {
                return usecase.GetUsers()
            })
        })

        user.GET("/:id", func(ctx *gin.Context) {
            framework(ctx, func() (interface{}, error) {
                id := ctx.Param("id")
                return usecase.GetUser(id)
            })
        })

        user.POST("/", func(ctx *gin.Context) {
            framework(ctx, func() (interface{}, error) {
                user := entity.User{}
                ctx.BindJSON(&user)
                return usecase.CreateUser(&user)
            })
        })

        user.DELETE("/:id", func(ctx *gin.Context) {
            framework(ctx, func() (interface{}, error) {
                id := ctx.Param("id")
                return usecase.DeleteUser(id)
            })
        })
    }

    router.Run(p.Address)
}

// コールバック使った共通処理をどうやるのかなと思って試してます。
// 一般的なエラーハンドリングではないかもしれないです。
func framework(ctx *gin.Context, handler func() (interface{}, error)) {
    ret, err := handler()
    if err != nil {
        // 共通のエラーハンドリング作ってみたり
        ctx.JSON(http.StatusInternalServerError, gin.H{"message": err})
        return
    }
    ctx.JSON(200, ret)
}

Web F/Wとして、 Gin を利用していますが、Ginに依存したコードをこのファイル内に限定させ、Domain(usecase)レイヤーに渡してません。
こうすることで、別のWeb F/Wに変更したり、 AWS Lambda などのサーバーレスに変更する際に Domain レイヤー以下の修正を無くすことができます。

エントリーポイント

DIコンテナとか調べなかったので、main関数で各I/Fに依存性を設定しています。

main.go
package main

func main() {
    d := initDB()
    // Repository には、DBの実装を設定
    repository.User = &db.UserRepositoryImpl{DB: d}
    // WebApiとしては Gin を利用
    presentation.Api = &presentation.GinApi{Address: ":8083"}
    presentation.Setup()
}

func initDB() *gorm.DB {
    d, _ := gorm.Open("mysql", "root@tcp(127.0.0.1:3306)/hogehoge")
    d.LogMode(true)
    return d
}

まとめ

Domainレイヤーが薄っぺら過ぎて、この規模だと Clean Architecture の旨味が全然分からないですね。。。
まぁサービスを立ち上げる場合はもっとビジネスロジックが入ってくるはずなので、その時に効果を発揮してくれるかなってことで、やり方確認できたから良しとしよう。

そんな感じで今回 Go をまったく触ったことなかったですが、割とサクッと作ることができました。
今回時間使ったのは、 Clean Architecture っぽいレイヤー構成にするのに、Interface
だクラスだの書き方が、JavaKotlinSwift なんかと結構違ったのでそういった所に戸惑いました。
ただサーバー作るだけならもっとサクッと出来そうです。

7
7
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
7
7