go + gin + gormでtodo Webアプリを作ってみた

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

go歴３ヶ月でtodo webアプリを作ってみました。


環境：Macbook + Goland

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

# ginのインストール
[ginの公式サイト](https://gin-gonic.com/)

```go get -u github.com/gin-gonic/gin```

## その他ライブラリのインストール
### 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/06048c2b-4ece-7669-56df-25a7cd624078.png" width="200" />

・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>
    <h1>
    {{ .title }}
    </h1>

    <form role="form" action="/todos/save" 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="/todos/edit?id={{.ID}}">{{ .ID }}:{{ .Content }}</a></p> <a href="/todos/destroy?id={{ .ID }}">[削除]</a>
    {{end}}
</html>
```
ファイル：templates/edit.html
todo変更フォーム
```
<html>
    <h1>
    {{ .title }}
    </h1>
    <form role="form" action="/todos/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>
```

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

```
package main

import (
	"github.com/gin-gonic/gin"
	"go_gin_todo/config"
	"go_gin_todo/models"
	"io"
	"log"
	"net/http"
	"os"
	"strconv"
)

func setupRouter() *gin.Engine {

	//ログ設定
	f, _ := os.Create(config.Config.LogFile)
	gin.DefaultWriter = io.MultiWriter(os.Stdout, f)

	//templateディレクトリ設定
	r := gin.Default()
	r.LoadHTMLGlob("templates/**/*")

	// todo create
	r.POST("/todos/save", func(c *gin.Context) {

		models.CreateTodo(c.PostForm("content"))

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

	// todo create
	r.POST("/todos/update", func(c *gin.Context) {
		id, _ := strconv.Atoi(c.PostForm("id"))
		content := c.PostForm("content")
		todo, _ := models.GetTodo(id)
		todo.Content = content
		models.UpdateTodo(todo)

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

	//todo edit
	r.GET("/todos/edit", func(c *gin.Context) {
		id, err := strconv.Atoi(c.Query("id"))
		if err != nil {
			log.Fatalln(err)
		}
		todo, _ := models.GetTodo(id)

		c.HTML(http.StatusOK, "edit.tmpl", gin.H{
			"title": "Todo",
			"todo":  todo,
		})
	})

	//todo delete
	r.GET("/todos/destroy", func(c *gin.Context) {
		id, err := strconv.Atoi(c.Query("id"))
		if err != nil {
			log.Fatalln(err)
		}
		models.DeleteTodo(id)

		c.Redirect(http.StatusMovedPermanently, "/todos/list")
	})
    //todo list
	r.GET("/todos/list", func(c *gin.Context) {

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

		c.HTML(http.StatusOK, "list.tmpl", gin.H{
			"title": "Todo",
			"todos": todos,
		})
	})

	return r
}

func main() {

	r := setupRouter()
	r.Run(":8080")
}
```

#### 実行イメージ

```go run main.go```

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

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/99447/41f2e2c2-b948-0523-99f2-bfac86205ff3.png" width="700" />

以上になります。

webアプリを作成するにはかなりお手軽なフレームワークかと思います。