go + Echo + gormでtodo Webアプリを最速で作ってみた

# go + Echo + gormでtodo Webアプリを最速で作ってみた

go + ginを使いまわしてEchoでTodoアプリを作ってみました。

環境：Macbook + Goland

#機能概要
* todoリスト
* todo作成
* todo更新
* todo削除

# Echoのインストール
[Echoの公式サイト](https://echo.labstack.com/)

```go get github.com/labstack/echo/v4```

## その他ライブラリのインストール
### GORM：O/Rマッパー
[gormの公式サイト](https://gorm.io/ja_JP/)
* gormのインストール
```go get -u gorm.io/gorm```
* gorm sqliteドライバのインストール
```go get -u gorm.io/driver/sqlite```

### configライブラリのインストール
```go get gopkg.in/ini.v1```

### todoアプリの開発
#### ディレクトリ構成
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/99447/32186e2f-e088-a210-6da9-88d5b3aba8c8.png">

・config
 コンフィグ設定ファイル
・models
 モデルファイル
・templates
 テンプレートディレクトリ

### configファイルを作成する
#### config.iniを編集する
```
[web]
logfile = webapp.log
[db]
driver = sqlite3
name = webapp.sql
```
#### コンフィグファイルをロードする
config.iniを変数にロードする
config/config.go
```
package config

import (
	"database/sql"
	"gopkg.in/ini.v1"
	"log"
)

type ConfigList struct {
	SQLDriver string
	DbName    string
	LogFile   string
}

var Db *sql.DB
var err error
var Config ConfigList

func init() {
	LoadConfig()
}

func LoadConfig() {
	cfg, err := ini.Load("config.ini")
	if err != nil {
		log.Fatalln(err)
	}
	Config = ConfigList{
		SQLDriver: cfg.Section("db").Key("driver").String(),
		DbName:    cfg.Section("db").Key("name").String(),
		LogFile:   cfg.Section("web").Key("logfile").String(),
	}
}
```

### モデルを作成する
ファイル：base.go
初期テーブルの作成やDBインスタンスの作成
```
package models

import (
	"fmt"
	_ "github.com/mattn/go-sqlite3"
	"go_gin_todo/config"
	"gorm.io/driver/sqlite"
	"gorm.io/gorm"
)

var Db *gorm.DB

var err error

const (
	tableNameTodo = "todos"
)

func init() {

	Db, err = gorm.Open(sqlite.Open(config.Config.DbName))

	if err != nil {
		fmt.Errorf("error:%v", err)
	}
	Db.AutoMigrate(&Todo{})

}
```
ファイル：todo.go
todoテーブルのO/RマッピングモデルとCRUD関数の定義を行う
```
package models

import (
	"gorm.io/gorm"
)

type Todo struct {
	gorm.Model
	Content string
}

func CreateTodo(content string) (err error) {

	todo := Todo{
		Content: content,
	}
	Db.Create(&todo)

	return err
}

func DeleteTodo(id int) (err error) {

	Db.Delete(&Todo{}, id)

	return err
}

func GetTodo(id int) (todo Todo, err error) {

	Db.Find(&todo, id)

	return todo, err
}

func UpdateTodo(t Todo) (err error) {

	Db.Save(&t)

	return err
}
```

### テンプレートファイルを作成する
ファイル：templates/todo/list.html
todoリストと作成フォームのhtml
```
<html>
<head>

</head>
<body>

<form role="form" action="/create" method="POST">
    <div class="lead">Todos作成</div>
    <div class="form-group">
        <textarea class="form-control" name="content" id="content" placeholder="Todoを追加" rows="4"></textarea>
        <br/>
        <br/>
        <button class="btn btn-lg btn-primary pull-right" type="submit">作成</button>
    </div>
</form>

<hr />
{{ range .todos }}
<p><a href="/edit?id={{.ID}}">{{ .ID }}:{{ .Content }}</a></p> <a href="/destroy?id={{ .ID }}">[削除]</a>
{{end}}

</body>
</html>
```
ファイル：templates/edit.html
todo変更フォーム
```
<html>
<h1>

</h1>
<form role="form" action="/update" method="POST">
    <input type="hidden" name="id" value="{{.todo.ID}}" />
    <div class="lead">Todos更新</div>
    <div class="form-group">
        <textarea class="form-control" name="content" id="content" placeholder="Todoを追加" rows="4">{{.todo.Content}}</textarea>
        <br/>
        <br/>
        <button class="btn btn-lg btn-primary pull-right" type="submit">更新</button>
    </div>
</form>
<hr />
</html>
```

### 処理関数を作成する
ファイル：server.go
todoのCRUD処理を行うメインファイル

```
package main

import (
	"github.com/labstack/echo/v4"
	"go_echo_todo/models"
	"html/template"
	"io"
	"log"
	"net/http"
	"strconv"
)

func main() {

	e := echo.New()

	renderer := &TemplateRenderer{
		templates: template.Must(template.ParseGlob("templates/*.html")),
	}

	e.Renderer = renderer

	e.GET("/", func(c echo.Context) error {
		return c.String(http.StatusOK, "Hello, World!")
	})

	e.GET("/list", list)
	e.POST("/create", create)
	e.GET("/edit", edit)
	e.POST("/update", update)
	e.GET("/destroy", destroy)

	e.Logger.Fatal(e.Start(":1323"))
}

type TemplateRenderer struct {
	templates *template.Template
}

func (t *TemplateRenderer) Render(w io.Writer, name string, data interface{}, c echo.Context) error {

	// Add global methods if data is a map
	if viewContext, isMap := data.(map[string]interface{}); isMap {
		viewContext["reverse"] = c.Echo().Reverse
	}

	return t.templates.ExecuteTemplate(w, name, data)
}

// delete
func destroy(c echo.Context) error {
	id, err := strconv.Atoi(c.QueryParam("id"))
	if err != nil {
		log.Fatalln(err)
	}
	models.DeleteTodo(id)

	return c.Redirect(http.StatusMovedPermanently, "/list")
}

// update
func update(c echo.Context) error {
	id, _ := strconv.Atoi(c.FormValue("id"))
	content := c.FormValue("content")
	todo, _ := models.GetTodo(id)
	todo.Content = content
	models.UpdateTodo(todo)

	return c.Redirect(http.StatusMovedPermanently, "/list")
}

// edit
func edit(c echo.Context) error {
	id, err := strconv.Atoi(c.QueryParam("id"))
	if err != nil {
		log.Fatalln(err)
	}
	todo, _ := models.GetTodo(id)

	data := map[string]interface{}{
		"todo": todo,
	}

	return c.Render(http.StatusOK, "edit.html", data)
}

// create
func create(c echo.Context) error {
	content := c.FormValue("content")

	models.CreateTodo(content)

	return c.Redirect(http.StatusMovedPermanently, "/list")
}

// list
func list(c echo.Context) error {

	var todos []models.Todo
	models.Db.Find(&todos)

	data := map[string]interface{}{
		"todos": todos,
	}

	return c.Render(http.StatusOK, "list.html", data)
}

```

#### 実行イメージ

```go run server.go```

ブラウザで以下を開く
http://localhost:1323/list

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/99447/dab7a0a3-9968-d761-5c78-6ea8ce0231d9.png" width="300" />


以上になります。


ありがとうございました。