Help us understand the problem. What is going on with this article?

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

今回は 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 なんかと結構違ったのでそういった所に戸惑いました。
ただサーバー作るだけならもっとサクッと出来そうです。

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away