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


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

* [KotlinでJersey+Exposedを使ったRESTサーバーを作る](https://qiita.com/nijuya_o/items/d1681b886fafc96f3525)
* [KotlinでJersey+JPAを使ったRESTサーバーを作る](https://qiita.com/nijuya_o/items/228a8af7fe12b1595c01)

それと、[Clean Architecture](https://qiita.com/nijuya_o/items/1699ffd6b30034c24815) の記事も書いたので、それらしいレイヤー構成にしてみます。

### 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
だクラスだの書き方が、`Java` や `Kotlin`、`Swift` なんかと結構違ったのでそういった所に戸惑いました。
ただサーバー作るだけならもっとサクッと出来そうです。