クリーンアーキテクチャ風味でGo！なTodoアプリ開発

# はじめに
どうも、水無月せきなです。
この記事は、前回の「[GoでTodoアプリを作ってみた（Gin×GORM×Docker）](https://qiita.com/MinadukiSekina/items/4d5036721e0074018f14)」の続きです。

今回は、実装の中身 ― 特にクリーンアーキテクチャ風な構成や、インターフェースの設計、ルーティング、フラッシュメッセージなど ― について具体的に掘り下げていきます。

Goによるアプリの開発としては珍しいかもしれないテンプレートとの連携まで書いているので、ぜひご覧ください。

# シリーズ一覧

1. [GoでTodoアプリを作ってみた（Gin × GORM × Docker）](https://qiita.com/MinadukiSekina/items/4d5036721e0074018f14)
1. クリーンアーキテクチャ風味でGo！なTodoアプリ開発
1. [GoでWebアプリのテストを書く（txdb × gomock × testify）](https://qiita.com/MinadukiSekina/items/cdd68177e75b3bb63899)

## リポジトリ
https://github.com/MinadukiSekina/todo-go-app

## 開発環境
Windows 10　22H2
WSL2
Docker Desktop 4.42.0
Cursor 1.1.3
Go 1.24.4
MySQL 8.0

# 実装の内容について
## アーキテクチャ

参考にした記事の多くがクリーンアーキテクチャで開発されたものが多かったため、図らずもそちらへ寄せたような形になっています。

前回の記事でも触れましたが、実装とテストにおいては、下記の記事を特に参考に（というか依拠）させていただきました。

https://qiita.com/ogady/items/34aae1b2af3080e0fec4#didependency-injection%E3%83%91%E3%82%BF%E3%83%BC%E3%83%B3%E3%81%A8%E3%81%AF

クリーンアーキテクチャの説明は他の方の[記事](https://blog.tai2.net/the_clean_architecture.html)に譲りますが、ざっくり言えば、レイヤーに分けることで関心を分離し依存の方向性を定めることで、保守性を向上させるアーキテクチャです。

本アプリにおいてはディレクトリ構成の点で徹底できていませんが、実装レベルでは、handler（`app/handlers/web/todoHandler.go`）やrepository（`app/db/todoRepository.go`）といった関心や責務の分離はできているのではないかと思います。

関心の分離やインターフェース経由での結合により、テストをしやすかったです。一方で、小規模なアプリの割にはコード量が多くなったり、定義に飛ぼうとしたらインターフェースの方に飛んだりと、若干の手間は感じました。

## 依存関係とDIについて

クリーンアーキテクチャにおいて、レイヤーという層状の構造の他に、依存の方向性も重要な要素かと思います。

この点に関しては、依存する実装を定義したインターフェースを構造体のフィールドとして保持すること、およびコンストラクタ（`New`で始まるメソッド）でそのインターフェースを実装したオブジェクトを受け取ることで実現しています。

依存先の渡し方については、DIによって実現しています。
（前述の参考記事の通りなので、詳しくはそちらもご参照下さい）

## ディレクトリ構造

※テストファイルなど一部のファイル・フォルダは省いています。

```
/app
|-- app
|   |-- db
|   |   |-- db.go
|   |   `-- todoRepository.go
|   |-- domain
|   |   |-- interfaces
|   |   |   `-- closer.go
|   |   |-- models
|   |   |   `-- todo.go
|   |   `-- repository
|   |       `-- todoRepository.go
|   |-- handlers
|   |   `-- web
|   |       |-- flash.go
|   |       |-- mainHandler.go
|   |       |-- route
|   |       |   `-- route.go
|   |       `-- todoHandler.go
|   |-- injector
|   |   `-- injector.go
|   |-- static
|   |   |-- css
|   |   |   `-- style.css
|   |   `-- js
|   |       `-- todoItemClick.js
|   |-- templates
|   |   |-- error
|   |   |   `-- error.html
|   |   `-- todo
|   |       |-- index.html
|   |       `-- show.html
|   `-- usecases
|       `-- todoUsecase.go
|-- go.mod
|-- go.sum
`-- main.go
```

主要なディレクトリの内容としては下記の通りです。
実装の内容はクリーンアーキテクチャ風になりましたが、ディレクトリの構成はそこまで至ってないですね。

| ディレクトリ | 内容 |
|:-:|:-:|
| db | DBに対する処理 |
| domain | クリーンアーキテクチャで言うエンティティ |
| handlers | ルーティング・リクエストの処理 |
| injector | DI用 |
| templates | ブラウザで表示する画面 |
| usecases | クリーンアーキテクチャで言うUse Cases |

ここからは、ディレクトリごとに実装をご紹介します。

## app/db
### db.go

[こちらの記事](https://zenn.dev/osushi02/articles/21e14cbcf71008#%E6%89%8B%E9%A0%864%3A-api%E4%BD%9C%E6%88%90)を参考にしたため、シングルトンっぽくなっています。
DBへの接続と終了処理が責務です。

<details><summary>実装の全体はこちら</summary>

```go:app/db/db.go
package db

import (
	"fmt"
	"os"

	"github.com/MinadukiSekina/todo-go-app/app/domain/interfaces"
	"github.com/MinadukiSekina/todo-go-app/app/domain/models"
	"gorm.io/driver/mysql"
	"gorm.io/gorm"
)

// データベース接続を管理するインターフェース
type SqlHandler interface {
	interfaces.Closer
	GetConnection() *gorm.DB
}

// データベース接続の実装を保持する構造体
type sqlHandler struct {
	conn        *gorm.DB
	initialized bool
}

// データベース接続を取得する
// 接続が初期化されていない場合は初期化を行う
func (handler *sqlHandler) GetConnection() *gorm.DB {
	if handler == nil || !handler.initialized {
		Init()
	}
	return handler.conn
}

// シングルトンインスタンスを保持するグローバル変数
var handler *sqlHandler

// データベース接続を初期化する
// 環境変数から接続情報を取得し、GORMを使用して接続を確立する
// 接続後、Todoモデルのマイグレーションを実行する
func Init() {
	if handler != nil && handler.initialized {
		return
	}

	// db.envに定義したDB関係の環境変数を取得
	dbUser := os.Getenv("MYSQL_USER")
	dbPassword := os.Getenv("MYSQL_PASSWORD")
	dbName := os.Getenv("MYSQL_DATABASE")
	// tcp（）の中にdocker-composeで定義したDB用コンテナのサービス名を入れれば、
	// 自動的にホストとポートを読み取ってくれる
	dsn := fmt.Sprintf(
		"%s:%s@tcp(db)/%s?charset=utf8mb4&parseTime=true&loc=Local",
		dbUser,
		dbPassword,
		dbName,
	)
	dB, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})

	if err != nil {
		panic("Could not connect to database.")
	}

	// マイグレーションを行う
	dB.AutoMigrate(&models.Todo{})

	handler = &sqlHandler{
		conn:        dB,
		initialized: true,
	}
}

// SqlHandlerのインスタンスを取得する
// インスタンスが存在しない場合は初期化を行う
func GetSqlHandler() *sqlHandler {
	if handler == nil || !handler.initialized {
		Init()
	}
	return handler
}

// ハンドラーの終了処理を行う
func (handler *sqlHandler) Close() error {
	if handler.conn == nil || !handler.initialized {
		return nil
	}
	db, err := handler.conn.DB()
	if err != nil {
		return err
	}
	err = db.Close()
	return err
}
```
</details>

### todoRepository.go

CRUDの処理を担う部分です。

ドキュメントを読んだりテストを実行した限りだと、どうやらGORMは存在しないIDでも処理を行おうとするorエラーを返さないようなので、UpdateやDeleteで存在のチェックをしています。

```go:UpdateとDelete
func (tr *todoRepository) Update(todo *models.Todo) error {
	// Saveメソッドだと、存在しないIDの場合はCreate動作になるため、
	// 存否チェックをする
	_, err := tr.FindById(todo.ID)
	if err != nil {
		return err
	}
	result := tr.handler.GetConnection().Save(todo)
	return result.Error
}

func (tr *todoRepository) Delete(id uint) error {
	// 存在しないIDの場合でもエラーは出ないようなので、存否チェックをする
	_, err := tr.FindById(id)
	if err != nil {
		return err
	}
	result := tr.handler.GetConnection().Delete(&models.Todo{}, id)
	return result.Error
}
```

<details><summary>実装の全体はこちら</summary>

```go:app/db/todoRepository.go
package db

import (
	"log/slog"

	"github.com/MinadukiSekina/todo-go-app/app/domain/models"
	"github.com/MinadukiSekina/todo-go-app/app/domain/repository"
)

// todoモデルのDB処理を担うリポジトリの構造体
type todoRepository struct {
	handler SqlHandler
}

// TodoRepositoryの新しいインスタンスを作成して返す
func NewTodoRepository(sqlHandler SqlHandler) repository.TodoRepository {
	todoRepository := todoRepository{handler: sqlHandler}
	return &todoRepository
}

// todoの一覧を返す
func (tr *todoRepository) FindAll() (*[]models.Todo, error) {
	var todos []models.Todo
	result := tr.handler.GetConnection().Find(&todos)
	return &todos, result.Error
}

// 指定されたIDのtodoを検索して結果を返す
func (tr *todoRepository) FindById(id uint) (*models.Todo, error) {
	var todo models.Todo
	result := tr.handler.GetConnection().Where("id = ?", id).First(&todo)
	if result.Error != nil {
		return nil, result.Error
	}
	return &todo, nil
}

// 渡されたtodoを新規作成して保存する
func (tr *todoRepository) Create(todo *models.Todo) error {
	result := tr.handler.GetConnection().Create(todo)
	return result.Error
}

// 渡されたtodoのデータを更新する
func (tr *todoRepository) Update(todo *models.Todo) error {
	// Saveメソッドだと、存在しないIDの場合はCreate動作になるため、
	// 存否チェックをする
	_, err := tr.FindById(todo.ID)
	if err != nil {
		return err
	}
	result := tr.handler.GetConnection().Save(todo)
	return result.Error
}

// 指定されたIDのtodoを削除する
func (tr *todoRepository) Delete(id uint) error {
	// 存在しないIDの場合でもエラーは出ないようなので、存否チェックをする
	_, err := tr.FindById(id)
	if err != nil {
		return err
	}
	result := tr.handler.GetConnection().Delete(&models.Todo{}, id)
	return result.Error
}

// todoRepositoryの終了処理
func (th *todoRepository) Close() error {
	// 依存先をクローズする
	err := th.handler.Close()
	if err != nil {
		slog.Error(err.Error())
	}
	return err
}
```
</details>

## app/domain/interfaces
アプリケーション全体で使用するインターフェイスです。

```go:app/domain/interfaces/closer.go
// 終了処理を行うためのインターフェイス
type Closer interface {
	Close() error
}
```

`Closer`インターフェースは、アプリケーションの終了時に行う処理を実装することを意図して作成しました。
一番の目的は、`db.go`におけるシングルトン的実装によって張りっぱなしになっているDBへのコネクションの解放です。

handlerやrepositoryなどのインターフェースにも埋め込みで継承させており、依存先の`Close()`を呼び出しながら自身の終了処理を行わせています。エラーが返る場合はログに記録し、処理自体は続行する方針です。

と言っても、DBの接続終了以外にすることが現時点では無いので、基本的に呼び出しているだけになっていますが……

<details><summary>Closeメソッドの連鎖的な呼び出し</summary>

```go:app/handlers/web/route/route.go
func SetRouting() {
	router := gin.Default()

	…省略…

	// 各ハンドラーの初期化
	th := injector.InjectTodoHandler()
	mh := injector.InjectMainHandler()

	// ハンドラーの終了処理
	defer th.Close()

	…省略…
    
	// 待機開始
	router.Run(":3000")
}
```

```go:app/handlers/web/todoHandler.go
// 終了処理を行う
func (th *TodoHandler) Close() {
	err := th.todoUsecase.Close()
	if err != nil {
		slog.Error(err.Error())
	}
}
```

```go:app/usecases/todoUsecase.go
// ユースケースのインターフェイス
type TodoUsecase interface {
	interfaces.Closer // Closerインターフェースの埋め込みによる継承
	SearchByID(uint) (*models.Todo, error)
	Show() (todos *[]models.Todo, err error)
	Add(todo *models.Todo) error
	Edit(todo *models.Todo) error
	Delete(id uint) error
}
</details>

// todoに関わるユースケースの構造体
type todoUsecase struct {
	repos repository.TodoRepository
}

…省略…

// ユースケースの終了処理を行う
func (uc *todoUsecase) Close() error {
	err := uc.repos.Close()
	if err != nil {
		slog.Error(err.Error())
	}
	return err
}

```

</details>

## app/domain/models

本来はバリデーションやロジックなども書くと思うのですが、Todoモデルの定義とステータスを文字列から変換する関数のみとなっています。変換する関数は、画面上のチェックボックスの値との変換で使用しています。

<details><summary>実装の全体はこちら</summary>

```go:app/domain/models/todo.go
package models

import (
	"errors"

	"gorm.io/gorm"
)

type Status int

const (
	invalid Status = iota - 1
	NotStarted
	Done
)

// todoのデータを保持する構造体
type Todo struct {
	gorm.Model
	Title  string
	Status Status
}

func StrToStatus(target string, correspond map[string]Status) (status Status, err error) {
	if target == "" {
		return invalid, errors.New("target string is empty")
	}
	if len(correspond) == 0 {
		return invalid, errors.New("correspond map is empty")
	}

	for key, value := range correspond {
		if key == target && NotStarted <= value && value <= Done {
			return value, nil
		}
	}
	return invalid, errors.New("invalid status value: " + target)
}
```

</details>

## app/handlers/web/route

WEBリクエストのルーティングを定めています。

```go:app/handlers/web/route/route.go
package route

import (
	"github.com/MinadukiSekina/todo-go-app/app/injector"
	"github.com/gin-gonic/gin"
)

// ルーティングやミドルウェアの設定を行う
func SetRouting() {
	router := gin.Default()

	// HTML・css・jsファイルの読み込み
	router.LoadHTMLGlob("app/templates/*/*.html")
	router.Static("/css", "app/static/css/")
	router.Static("/js", "app/static/js/")

	// 各ハンドラーの初期化
	th := injector.InjectTodoHandler()
	mh := injector.InjectMainHandler()

	// ハンドラーの終了処理
	defer th.Close()

	// ルーティングの設定
	router.GET("/", mh.Index)

	router.GET("/todo", th.Index)
	router.POST("/todo", th.Create)

	router.GET("/todo/:id", th.ShowById)
	router.POST("/todo/:id", th.Update)
	router.POST("/todo/:id/delete", th.Delete)

	// 待機開始
	router.Run(":3000")
}
```
`LoadHTMLGlob`でHTMLを、`Static`でCSSやJavaScriptを読み込んでいます。
これをしないと、ページが表示できません。

`Static`の第一引数は、HTMLから参照する際のパスの始点、第二引数がファイルのあるディレクトリへのパスです。

**参考記事**

https://qiita.com/suzakutakumi3/items/123460ba5d24a8210228

## app/handlers/web
`gin.context`を受け取ってリクエストを処理します。
また、フラッシュメッセージに関わるものもここに置いています。

### todoHandler.go
`gin.context`も抽象化してインターフェースで受けることもあるようですが、そこまでしなくても良いかと思って、そのまま受け取っています。

すべてのハンドラー関数を説明すると長くなるので、いくつかに絞ります。

#### Index：一覧表示

```go:app/handlers/web/todoHandler.go
// todoの一覧を表示する
func (th *TodoHandler) Index(c *gin.Context) {
	todos, err := th.todoUsecase.Show()
	if err != nil {
		c.HTML(http.StatusInternalServerError, "error/error.html", gin.H{
			"message": err.Error(),
		})
		return
	}

	// Sort todos by status in ascending order
	sort.Slice(*todos, func(i, j int) bool {
		return (*todos)[i].Status < (*todos)[j].Status
	})

	// Get flash message content if it exists
	fm := GetFlashMessage(c)

	// テンプレートを呼び出して値を渡す
	c.HTML(http.StatusOK, "todo/index.html", gin.H{
		"todos":      todos,
		"NotStarted": models.NotStarted,
		"Done":       models.Done,
		flashMessage: fm.Message,
		flashType:    fm.Type,
	})
}
```

データの取得時にエラーが返ってきた場合はエラー画面を、それ以外の場合は一覧の画面を返しています。

後述する通り、画面上でTodoのステータスを参照しているため、列挙型として定義している`NotStarted`と`Done`の値もテンプレートに渡しています。

```HTML:app/templates/todo/index.html
{{ define "todo/index.html" }}
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Todo一覧</title>
    <link href="css/style.css" rel="stylesheet">
    <script src="/js/todoItemClick.js" defer></script>
</head>
<body>
    <div class="todo-list">
        <div class="header">
            <h1>Todo一覧</h1>
        </div>
        {{if .Message}}
        <div class="flash">
            <div class="flash-message flash-{{.Type}}">
                <p>{{.Message}}</p>
            </div>
        </div>
        {{end}}
        <div class="todo-form">
            <form method="post" action="/todo">
                <div class="form-row">
                    <div class="form-group">
                        <label for="title">新しいタスク</label>
                        <input type="text" id="title" name="title" class="form-control" placeholder="タスクのタイトルを入力してください" required />
                    </div>
                </div>
                <button type="submit" class="btn btn-primary">追加</button>
            </form>
        </div>
        {{ if gt (len .todos) 0 }}
            {{ range .todos }}
            <div class="todo-item" data-id="{{.ID}}">
                <span class="todo-title">{{ .Title }}</span>
                <span class="todo-status {{ if eq .Status $.NotStarted }}status-pending{{ else }}status-completed{{ end }}">
                    {{ if eq .Status $.NotStarted }}未完了{{ else }}完了{{ end }}
                </span>
            </div>
            {{ end }}
        {{ else }}
            <div class="no-items">
                <p class="no-items-message">Todoはありません。</p>
                <p class="no-items-message">フォームから新しいタスクを追加してください。</p>
            </div>
        {{ end }}
    </div>
</body>
</html>
{{ end }}
```

テンプレート側では、`range .todos`で一覧の行を作成しています。

ステータスを表示する箇所で`if eq .Status $.NotStarted`と書いていますが、これは`$`がハンドラーから渡されたデータを指すことを利用して、`todo.Status`と`NotStarted`との比較を行っています。`$`を外すと正常に動作しません。

#### Create : 新規作成

```go:app/handlers/web/todoHandler.go
// todoを新規作成する
func (th *TodoHandler) Create(c *gin.Context) {
	title := c.PostForm("title")
	todo := models.Todo{Title: title, Status: models.NotStarted}
	err := th.todoUsecase.Add(&todo)
	if err != nil {
		SetFlashMessage(c, resultIsError, "新しいタスクの作成に失敗しました。")
		c.Redirect(http.StatusSeeOther, "/todo")
		return
	}
	SetFlashMessage(c, resultIsSuccess, "新しいタスクを作成しました。")
	c.Redirect(http.StatusFound, "/todo")
}
```

POSTで受けたデータからタイトルを取得して新規作成し、その後一覧画面にリダイレクトしています。
リダイレクトする前に、次で説明するフラッシュメッセージを設定しています。

<details><summary>実装の全体はこちら</summary>

```go:app/handlers/web/todoHandler.go
package handlers

import (
	"log/slog"
	"net/http"
	"sort"
	"strconv"

	"github.com/MinadukiSekina/todo-go-app/app/domain/models"
	"github.com/MinadukiSekina/todo-go-app/app/usecases"
	"github.com/gin-gonic/gin"
)

// todoパスへのリクエストに対するハンドラーの構造体
type TodoHandler struct {
	todoUsecase usecases.TodoUsecase
}

// TodoHandlerの新しいインスタンスを作成して返す
func NewTodoHandler(uc usecases.TodoUsecase) TodoHandler {
	todoHandler := TodoHandler{todoUsecase: uc}
	return todoHandler
}

// todoの一覧を表示する
func (th *TodoHandler) Index(c *gin.Context) {
	todos, err := th.todoUsecase.Show()
	if err != nil {
		c.HTML(http.StatusInternalServerError, "error/error.html", gin.H{
			"message": err.Error(),
		})
		return
	}

	// Sort todos by status in ascending order
	sort.Slice(*todos, func(i, j int) bool {
		return (*todos)[i].Status < (*todos)[j].Status
	})

	// Get flash message content if it exists
	fm := GetFlashMessage(c)

	c.HTML(http.StatusOK, "todo/index.html", gin.H{
		"todos":      todos,
		"NotStarted": models.NotStarted,
		"Done":       models.Done,
		flashMessage: fm.Message,
		flashType:    fm.Type,
	})
}

// 指定されたIDのtodoを表示する
func (th *TodoHandler) ShowById(c *gin.Context) {
	id_s := c.Param("id")
	id, err := strconv.ParseUint(id_s, 10, 64)
	if err != nil {
		SetFlashMessage(c, resultIsError, "このタスクは閲覧できません。")
		c.Redirect(http.StatusSeeOther, "/todo")
		return
	}
	todo, err := th.todoUsecase.SearchByID(uint(id))
	if err != nil {
		SetFlashMessage(c, resultIsError, "該当するタスクが見つかりませんでした。")
		c.Redirect(http.StatusSeeOther, "/todo")
		return
	}

	// Get flash message content if it exists
	fm := GetFlashMessage(c)

	c.HTML(http.StatusOK, "todo/show.html", gin.H{
		"todo":       todo,
		"NotStarted": models.NotStarted,
		"Done":       models.Done,
		flashMessage: fm.Message,
		flashType:    fm.Type,
	})
}

// todoを新規作成する
func (th *TodoHandler) Create(c *gin.Context) {
	title := c.PostForm("title")
	todo := models.Todo{Title: title, Status: models.NotStarted}
	err := th.todoUsecase.Add(&todo)
	if err != nil {
		SetFlashMessage(c, resultIsError, "新しいタスクの作成に失敗しました。")
		c.Redirect(http.StatusSeeOther, "/todo")
		return
	}
	SetFlashMessage(c, resultIsSuccess, "新しいタスクを作成しました。")
	c.Redirect(http.StatusFound, "/todo")
}

func (th *TodoHandler) Update(c *gin.Context) {
	id_s := c.Param("id")
	id, err := strconv.ParseUint(id_s, 10, 64)
	if err != nil {
		SetFlashMessage(c, resultIsError, "このタスクは更新できません。")
		c.Redirect(http.StatusSeeOther, "/todo")
		return
	}
	title := c.PostForm("title")
	status_s := c.PostForm("status")

	// 文字列のstatusをStatus型に変換
	var correspond = map[string]models.Status{
		"notStarted": models.NotStarted,
		"completed":  models.Done,
	}
	status, err := models.StrToStatus(status_s, correspond)
	if err != nil {
		SetFlashMessage(c, resultIsError, "タスクの状態が不正な値です。")
		c.Redirect(http.StatusSeeOther, "/todo/"+id_s)
		return
	}

	// 既存のTodoを取得
	existingTodo, err := th.todoUsecase.SearchByID(uint(id))
	if err != nil {
		SetFlashMessage(c, resultIsError, "対象となるタスクが存在しません。")
		c.Redirect(http.StatusSeeOther, "/todo")
		return
	}

	existingTodo.Title = title
	existingTodo.Status = status
	err = th.todoUsecase.Edit(existingTodo)
	if err != nil {
		SetFlashMessage(c, resultIsError, "タスクの内容を更新できませんでした。")
		c.Redirect(http.StatusSeeOther, "/todo/"+id_s)
		return
	}
	SetFlashMessage(c, resultIsSuccess, "タスクの内容を更新しました。")
	c.Redirect(http.StatusFound, "/todo/"+id_s)
}

// todoを削除する
func (th *TodoHandler) Delete(c *gin.Context) {
	id_s := c.Param("id")
	id, err := strconv.ParseUint(id_s, 10, 64)
	if err != nil {
		SetFlashMessage(c, resultIsError, "このタスクは削除できません。")
		c.Redirect(http.StatusSeeOther, "/todo")
		return
	}

	err = th.todoUsecase.Delete(uint(id))

	if err != nil {
		SetFlashMessage(c, resultIsError, "削除できませんでした。")
		c.Redirect(http.StatusSeeOther, "/todo/"+id_s)
		return
	}
	SetFlashMessage(c, resultIsSuccess, "タスクの削除を完了しました。")
	c.Redirect(http.StatusFound, "/todo")
}

// 終了処理を行う
func (th *TodoHandler) Close() {
	err := th.todoUsecase.Close()
	if err != nil {
		slog.Error(err.Error())
	}
}
```

</details>

### Flash Message
Railsのように処理後のメッセージを表示したいと思い、下記の記事を参考にして作成しました。

https://qiita.com/CoGee/items/17ae184d62afb37dbe84

```go:app/handlers/web/flash.go
package handlers

import (
	"github.com/gin-gonic/gin"
)

// フラッシュメッセージの内容を示す構造体
type FlashMessage struct {
	Type    string
	Message string
}

// cookieにフラッシュメッセージの内容を保存する
func SetFlashMessage(c *gin.Context, status string, msg string) {
	c.SetCookie(flashType, status, 1, "/", "localhost", true, true)
	c.SetCookie(flashMessage, msg, 1, "/", "localhost", true, true)
}

// cookieからフラッシュメッセージの内容を取り出して構造体に変換する
func GetFlashMessage(c *gin.Context) FlashMessage {
	t, _ := c.Cookie(flashType)
	m, _ := c.Cookie(flashMessage)
	return FlashMessage{Type: t, Message: m}
}

// フラッシュメッセージの種別
const (
	resultIsSuccess string = "success"
	resultIsError   string = "error"
)

// cookieに設定する値の名前
const (
	flashType    string = "Type"
	flashMessage string = "Message"
)
```

やっていることとしては単純で、短い生存時間のcookieを作成してそこにメッセージを保存し、リダイレクト先のハンドラー関数でcookieからメッセージを取得してテンプレートに渡すことで表示しています。

```go:app/handlers/web/todoHandler.go
// todoの一覧を表示する
func (th *TodoHandler) Index(c *gin.Context) {
	
    …省略…
	
    // この部分
	fm := GetFlashMessage(c)

	// 取り出したメッセージもテンプレートに渡す
	c.HTML(http.StatusOK, "todo/index.html", gin.H{
		"todos":      todos,
		"NotStarted": models.NotStarted,
		"Done":       models.Done,
		flashMessage: fm.Message,
		flashType:    fm.Type,
	})
}

…省略…

// todoを新規作成する
func (th *TodoHandler) Create(c *gin.Context) {
	
    …省略…
    
    // フラッシュメッセージをcookieに保存
	SetFlashMessage(c, resultIsSuccess, "新しいタスクを作成しました。")
	c.Redirect(http.StatusFound, "/todo")
}
```

```HTML:app/templates/todo/index.html
    …省略…

    {{if .Message}}
    <div class="flash">
        <div class="flash-message flash-{{.Type}}">
            <p>{{.Message}}</p>
        </div>
    </div>
    {{end}}
    
    …省略…
```

## app/injector

依存性の解決において、下記に引用した考えで行います。

https://inukirom.hatenablog.com/entry/di-in-go

> * オブジェクトを引数0個で取得できるようにする
> * オブジェクトのコンストラクタに対して他のInject関数を使ってオブジェクトを注入する
> 
> あるオブジェクトについて、依存先が引数0個で取得できれば、そのオブジェクトも引数0個で取得できるので、これを組み合わせるというものです。

```go:app/injector/injector.go
package injector

import (
	"github.com/MinadukiSekina/todo-go-app/app/db"
	"github.com/MinadukiSekina/todo-go-app/app/domain/repository"
	handlers "github.com/MinadukiSekina/todo-go-app/app/handlers/web"
	"github.com/MinadukiSekina/todo-go-app/app/usecases"
)

// データベース接続を初期化し、SqlHandlerを返す
func InjectDB() db.SqlHandler {
	sqlhandler := db.GetSqlHandler()
	return sqlhandler
}

// sqlHandlerを使用してTodoRepositoryを生成する
func InjectTodoRepository() repository.TodoRepository {
	sqlHandler := InjectDB()
	return db.NewTodoRepository(sqlHandler)
}

// TodoRepositoryを使用してTodoUsecaseを生成する
func InjectTodoUsecase() usecases.TodoUsecase {
	TodoRepo := InjectTodoRepository()
	return usecases.NewTodoUsecase(TodoRepo)
}

// TodoUsecaseを使用してTodoHandlerを生成する
func InjectTodoHandler() handlers.TodoHandler {
	return handlers.NewTodoHandler(InjectTodoUsecase())
}

// アプリケーションのメインハンドラー（ルートパス用）を生成する
func InjectMainHandler() handlers.MainHandler {
	return handlers.NewMainHandler()
}
```

ここで定義した`inject~`のメソッドを呼ぶことで、ドミノ倒し的なDIを行っています。

```go:DIの箇所（app/handlers/web/route/route.go）
func SetRouting() {
	…省略…

	// DIの実行
	th := injector.InjectTodoHandler()
	mh := injector.InjectMainHandler()

	…省略…
}
```



## app/usecases
ユースケースレベルの実装部分です。
現時点だと複数のモデルが絡むようなことは無いため、Repositoryに処理を渡して結果をそのまま返しています。

<details><summary>実装の全体はこちら</summary>

```go:app/usecases/todoUsecase.go
package usecases

import (
	"log/slog"

	"github.com/MinadukiSekina/todo-go-app/app/domain/interfaces"
	"github.com/MinadukiSekina/todo-go-app/app/domain/models"
	"github.com/MinadukiSekina/todo-go-app/app/domain/repository"
)

// ユースケースのインターフェイス
type TodoUsecase interface {
	interfaces.Closer
	SearchByID(uint) (*models.Todo, error)
	Show() (todos *[]models.Todo, err error)
	Add(todo *models.Todo) error
	Edit(todo *models.Todo) error
	Delete(id uint) error
}

// todoに関わるユースケースの構造体
type todoUsecase struct {
	repos repository.TodoRepository
}

// TodoUsecaseの新しいインスタンスを作成して返す
func NewTodoUsecase(todoRepo repository.TodoRepository) TodoUsecase {
	todoUsecase := todoUsecase{repos: todoRepo}
	return &todoUsecase
}

// 指定されたIDのtodoを検索して結果を返す
func (uc *todoUsecase) SearchByID(id uint) (todo *models.Todo, err error) {
	todo, err = uc.repos.FindById(id)
	return
}

// todoの一覧を検索して返す
func (uc *todoUsecase) Show() (todos *[]models.Todo, err error) {
	todos, err = uc.repos.FindAll()
	return
}

// 渡されたtodoを新規作成して保存する
func (uc *todoUsecase) Add(todo *models.Todo) (err error) {
	err = uc.repos.Create(todo)
	return
}

// 渡されたtodoを更新して保存する
func (uc *todoUsecase) Edit(todo *models.Todo) (err error) {
	err = uc.repos.Update(todo)
	return
}

// 指定されたIDのtodoを削除する
func (uc *todoUsecase) Delete(id uint) (err error) {
	err = uc.repos.Delete(id)
	return
}

// ユースケースの終了処理を行う
func (uc *todoUsecase) Close() error {
	err := uc.repos.Close()
	if err != nil {
		slog.Error(err.Error())
	}
	return err
}
```

</details>

## app/templates
実際に表示される画面です。CSSとJavaScriptは`app/static`に分離しています。
一覧表示の画面は既に掲出していますので、[そちら](https://qiita.com/drafts/aa23130f04c6ec936854/edit#index%E4%B8%80%E8%A6%A7%E8%A1%A8%E7%A4%BA)をご参照ください。

特に変なことはしていないですが、詳細画面において「更新」と「削除」を一つのフォームで対応するために、`submit`を二つ用意しています。ボタンの`formaction`でPOSTするパスを指定することで処理を分けました。

**参考記事**

https://simple-minds-think-alike.hatenablog.com/entry/html5-formaction-formmethod

```html:app/templates/todo/show.htmlから抜粋
…省略…

<div class="todo-form">
    <form method="post">
        <div class="form-row">
            <div class="form-group">
                <label for="title">タスクのタイトル</label>
                <input type="text" id="title" name="title" class="form-control" placeholder="タスクのタイトルを入力してください" required value="{{.todo.Title}}" />
            </div>
        </div>
        <div class="form-row">
            <div class="form-group">
                <label>タスクの状態</label>
                <div class="radio-group">
                    <label class="radio-label">
                        <input type="radio" name="status" value="notStarted" {{ if eq .todo.Status $.NotStarted }}checked{{end}}>
                        未完了
                    </label>
                    <label class="radio-label">
                        <input type="radio" name="status" value="completed" {{ if eq .todo.Status $.Done }}checked{{end}}>
                        完了
                    </label>
                </div>
            </div>
        </div>
        <div style="display: flex; justify-content: space-between; align-items: center;">
            <button type="submit" class="btn btn-primary" formaction="/todo/{{.todo.ID}}">更新</button>
            <button type="submit" class="btn btn-danger" formaction="/todo/{{.todo.ID}}/delete" onclick="return confirm('このタスクを削除してもよろしいですか？')">削除</button>
        </div>
    </form>
</div>

…省略…

```

ちなみに、`tamplate`直下以外の場所にファイルがある場合は、`{{define <template-path>}} {{end}}`ブロックで囲う必要がありそうでした。

https://gin-gonic.com/en/docs/examples/html-rendering/

わかりやすさのためにも、`tamplate`直下のファイルでも囲っておいた方が良いかもしれません。

# おわりに
ここまでお読みいただき、ありがとうございました。

長々と実装について説明してきましたが、改善点のようなものが自分でも見えた気がしますし、モデルのリレーションもやってみたいので、今後少しずつ手を加えるかもしれません。

[次回](url)は本シリーズの最終回、テスト編です。
良ければ合わせてお読みください。

この記事が参考になれば幸いです。

# 参考資料

## アプリケーション全般
* [Go / Gin で超簡単なWebアプリを作る](https://qiita.com/hyo_07/items/59c093dda143325b1859)
* [Go言語でTODOアプリwithクリーンアーキテクチャ](https://qiita.com/kikils/items/50be7c65664d867f3a1a)
* [【Go + レイヤードアーキテクチャ】DDDを意識してWeb APIを実装してみる](https://tech.yyh-gl.dev/blog/go_web_api/)
* [Clean ArchitectureでAPI Serverを構築してみる](https://qiita.com/hirotakan/items/698c1f5773a3cca6193e)
* [Go言語でClean Architectureを実現して、gomockでテストしてみた](https://qiita.com/ogady/items/34aae1b2af3080e0fec4)
* [GoにおけるDI](https://inukirom.hatenablog.com/entry/di-in-go)

## GORM関連
* [GORMガイド](https://gorm.io/ja_JP/docs/index.html)
* [GORMを使ってGoでデータベース操作の説明](https://qiita.com/atsutama/items/68773112208c7fc05069)
* [How to use Enums with GORM!](https://techwasti.com/how-to-use-enums-with-gorm)
* [Docker環境にGo(Air/Echo/Gorm)でAPIを作る](https://zenn.dev/osushi02/articles/21e14cbcf71008)

## Gin関連
* [【Go】GinでURLからパラメーターを受け取る2つの方法
](https://qiita.com/holy_engineer/items/42ecc5c985188e997adc)
* [【Golang】Ginを使ってフラッシュメッセージを表示させる](https://qiita.com/CoGee/items/17ae184d62afb37dbe84)
* [ginでフォーム、JSONのリクエストを受け取る](https://nansystem.com/gin-get-post-json-form/)
* [備忘録 Goでginを使う時に、cssやjsのファイルが読み込めない問題の解決](https://qiita.com/suzakutakumi3/items/123460ba5d24a8210228)
* [gin.Contextを引数にもつ関数をunitテストする](https://qiita.com/takehanKosuke/items/849c732c5892d149b50a)
* [Unit testing of gin's Context.Redirect works for GET response code but fails for POST response code (golang)](https://stackoverflow.com/questions/76319196/unit-testing-of-gins-context-redirect-works-for-get-response-code-but-fails-for)

## その他
* [【HTML5】1つのフォームに複数のSubmitボタンをつけるには(formaction, formmethod)](https://www.moritamorie.com/entry/html5-formaction-formmethod)



[^1]:GETリクエストでも、`Content-Type`がヘッダーにある場合は同様らしいですが
[^2]:本番で問題ないのか？ と思われる方もいらっしゃるかもしれませんが、本番だと処理の際に`context.Writer.WriteHeaderNow`が呼ばれているようです : https://github.com/gin-gonic/gin/blob/6a0556ed5a67d1d12ae3e7ea2c0121b6c3b894e2/gin.go#L621