はじめに
今年(2019年)の5月からチーム内の勉強会が週1〜2ペースで開催されるようになり、
モダンな言語を新たに習得したかったため積極的に参加して学習を進めてきました。
基本的なコードを読めるようになり、更なる理解を深めようと次のステップとして、
お決まりですがToDoアプリを作成したので手順などをまとめておきます。
今回GinとGORMを採用した理由は参考にできる記事や公式のドキュメントが充実しているからです。
勉強会について
半年で進めていた内容は以下の通りです。
-
A Tour of Go
→読書会形式で皆で内容を読み進め、playgrond上でコードをし挙動を確認し進めました。 -
Writing Web Applications
→ 講義形式(ハンズオン)で実装を進めました。 -
GCPのチュートリアル
→講義形式(ハンズオン)で実装を進めました。 -
AtCoder
→低難度の問題を繰り返し解き、基本構文(変数宣言、関数宣言、slice、for文など)の記述方法に慣れました。
今回の勉強会の参加者は全員Golang未経験だったため、ファシリテーター(進行役)が説明する講義・発表会形式ではなく、皆で資料の内容を読みながら疑問点を洗い出し、Googleで検索して疑問を解消するような流れが多かったです。
これは勉強会が講義・発表会形式だとファシリテーターが資料や教材の準備などが負担になり勉強会が開催されなくなるケースを防ぐためです。
とにかく一週間に一度はGoに触れようというコンセプトで今も勉強会を継続しています。
開発環境
- MacBook Pro
- go version : 1.12.9
$ go version
go version go1.12.9 darwin/amd64
事前情報
Webフレームワーク : Gin
ORM : GORM
DB : sqlite3
依存関係管理ツールdep : dep
├── Gopkg.lock
├── Gopkg.toml
├── README.md
├── controllers
│ └── task_controller.go
├── db
│ └── db.go
├── main.go
├── models
│ └── task.go
├── router
│ └── router.go
├── task.db
├── templates
│ ├── edit.html
│ └── index.html
└── vendor
ソースコードはGitHubにアップロードしています。
プロジェクトフォルダの作成
# $GOPATH + github.com + user名の配下にプロジェクトを作成
# ~/go/src/github.com/hanadaUG/go-gin-gorm-todo-app
$ cd go/src/github.com/hanadaUG/
$ mkdir go-gin-gorm-todo-app
$ cd go-gin-gorm-todo-app
depで外部パッケージを管理する
# macにdepをインストール
$ brew install dep
# 初期化
$ dep init
# 外部パッケージをimportした時、vendor配下にインストールする
$ dep ensure
main.go
Webアプリの起動起点となるmain.go
ではDBの初期化+Open処理、
ルーティングの設定のみを行うシンプルな構造になっています。
package main
import (
"github.com/hanadaUG/go-gin-gorm-todo-app/db"
"github.com/hanadaUG/go-gin-gorm-todo-app/router"
)
func main() {
// DBのOpen & Close処理の遅延実行
db.Initialize()
defer db.Close()
// ルーティング設定
router.Router()
}
db/db.go
DBのOpen/Close処理の制御はmain.go
から行うことを想定しているため
publicな関数(関数名を大文字から始める)として定義しています。
Get関数はDBのインスタンスを外部から取得するために用意しました。
package db
import (
"github.com/hanadaUG/go-gin-gorm-todo-app/models"
"github.com/jinzhu/gorm"
_ "github.com/mattn/go-sqlite3"
)
var db *gorm.DB
func Initialize() {
// 宣言済みのグローバル変数dbをshort variable declaration(:=)で初期化しようとすると
// ローカル変数dbを初期化することになるので注意する
// DBのコネクションを接続する
db, _ = gorm.Open("sqlite3", "task.db")
//if err != nil {
// panic("failed to connect database\n")
//}
// ログを有効にする
db.LogMode(true)
// Task構造体(Model)を元にマイグレーションを実行する
db.AutoMigrate(&models.Task{})
}
func Get() *gorm.DB {
return db
}
// DBのコネクションを切断する
func Close() {
db.Close()
}
models/task.go
ToDoアプリに登録する1タスクの定義のみ記述しています。
gorm.Model
構造体を埋め込み(embedded)、
Textというフィールド変数を追加しています。
※ タスクの状態を保持するStateというフィールド変数を後日追加します。
package models
import "github.com/jinzhu/gorm"
type Task struct {
gorm.Model
Text string
}
controllers/task_controller.go
CRUDの挙動を記述しています。
templateからの入力を受け、出力内容を制御します。
package controllers
import (
"github.com/gin-gonic/gin"
"github.com/hanadaUG/go-gin-gorm-todo-app/models"
"github.com/jinzhu/gorm"
"net/http"
)
type TaskHandler struct {
Db *gorm.DB
}
// 一覧表示
func (handler *TaskHandler) GetAll(c *gin.Context) {
var tasks []models.Task // レコード一覧を格納するため、Task構造体のスライスを変数宣言
handler.Db.Find(&tasks) // DBから全てのレコードを取得する
c.HTML(http.StatusOK, "index.html", gin.H{"tasks": tasks}) // index.htmlに全てのレコードを渡す
}
// 新規作成
func (handler *TaskHandler) Create(c *gin.Context) {
text, _ := c.GetPostForm("text") // index.htmlからtextを取得
handler.Db.Create(&models.Task{Text: text}) // レコードを挿入する
c.Redirect(http.StatusMovedPermanently, "/")
}
// 編集画面
func (handler *TaskHandler) Edit(c *gin.Context) {
task := models.Task{} // Task構造体の変数宣言
id := c.Param("id") // index.htmlからidを取得
handler.Db.First(&task, id) // idに一致するレコードを取得する
c.HTML(http.StatusOK, "edit.html", gin.H{"task": task}) // edit.htmlに編集対象のレコードを渡す
}
// 更新
func (handler *TaskHandler) Update(c *gin.Context) {
task := models.Task{} // Task構造体の変数宣言
id := c.Param("id") // edit.htmlからidを取得
text, _ := c.GetPostForm("text") // edit.htmlからtextを取得
handler.Db.First(&task, id) // idに一致するレコードを取得する
task.Text = text // textを上書きする
handler.Db.Save(&task) // 指定のレコードを更新する
c.Redirect(http.StatusMovedPermanently, "/")
}
// 削除
func (handler *TaskHandler) Delete(c *gin.Context) {
task := models.Task{} // Task構造体の変数宣言
id := c.Param("id") // index.htmlからidを取得
handler.Db.First(&task, id) // idに一致するレコードを取得する
handler.Db.Delete(&task) // 指定のレコードを削除する
c.Redirect(http.StatusMovedPermanently, "/")
}
router/router.go
ルーティングの設定を行います。
リクエストに対して、controllers/task_controller.go
で定義した振る舞いを割り当てます。
package router
import (
"github.com/gin-gonic/gin"
"github.com/hanadaUG/go-gin-gorm-todo-app/controllers"
"github.com/hanadaUG/go-gin-gorm-todo-app/db"
)
func Router() {
// gin内で定義されているEngine構造体インスタンスを取得
// Router、HTML Rendererの機能を内包している
router := gin.Default()
// globパターンに一致するHTMLファイルをロードしHTML Rendererに関連付ける
// 今回のケースではtemplatesディレクトリ配下のhtmlファイルを関連付けている
router.LoadHTMLGlob("templates/*.html")
// TaskHandler構造体に紐付けたCRUDメソッドを呼び出す
handler := controllers.TaskHandler{
db.Get(),
}
// 各パスにGET/POSTメソッドでリクエストされた時のレスポンスを登録する
// 第一引数にパス、第二引数にHandlerを登録する
router.GET("/", handler.GetAll) // 一覧表示
router.POST("/", handler.Create) // 新規作成
router.GET("/:id", handler.Edit) // 編集画面
router.POST("/update/:id", handler.Update) // 更新
router.POST("/delete/:id", handler.Delete) // 削除
// Routerをhttp.Serverに接続し、HTTPリクエストのリスニングとサービスを開始する
router.Run()
}
templates/index.html
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>TODOアプリ(Golang * gin * gorm)</title>
</head>
<body>
<form method="POST" action="/">
Task : <input type="text" name="text" value="">
<input type="submit" value="登録">
</form>
<br>
<ul>
{{ range .tasks }}
<li>
<form method="POST" action="/delete/{{.ID}}">
{{.ID}} : {{ .Text }} [<a href="/{{.ID}}">編集</a>]
<input type="submit" value="削除">
</form>
</li>
{{end}}
</ul>
</body>
</html>
templates/edit.html
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>TODOアプリ(Golang * gin * gorm)</title>
</head>
<body>
<form action="/update/{{.task.ID}}" method="POST">
<input type="text" name="text" value="{{.task.Text}}"><br>
<input type="submit" value="更新">
</form>
</body>
</html>
動作確認
$ go run main.go
[GIN-debug] [WARNING] Creating an Engine instance with the Logger and Recovery middleware already attached.
[GIN-debug] [WARNING] Running in "debug" mode. Switch to "release" mode in production.
- using env: export GIN_MODE=release
- using code: gin.SetMode(gin.ReleaseMode)
[GIN-debug] Loaded HTML Templates (3):
- edit.html
- index.html
-
[GIN-debug] GET / --> github.com/hanadaUG/go-gin-gorm-todo-app/controllers.(*TaskHandler).GetAll-fm (3 handlers)
[GIN-debug] POST / --> github.com/hanadaUG/go-gin-gorm-todo-app/controllers.(*TaskHandler).Create-fm (3 handlers)
[GIN-debug] GET /:id --> github.com/hanadaUG/go-gin-gorm-todo-app/controllers.(*TaskHandler).Edit-fm (3 handlers)
[GIN-debug] POST /update/:id --> github.com/hanadaUG/go-gin-gorm-todo-app/controllers.(*TaskHandler).Update-fm (3 handlers)
[GIN-debug] POST /delete/:id --> github.com/hanadaUG/go-gin-gorm-todo-app/controllers.(*TaskHandler).Delete-fm (3 handlers)
[GIN-debug] Environment variable PORT is undefined. Using port :8080 by default
[GIN-debug] Listening and serving HTTP on :8080
http://localhost:8080 にブラウザでアクセスして動作を確認できます。
ソースコードはGitHubにアップロードしています。
参考
下記サイト、記事を参考にさせていただきました。
- Go言語 GORM+GinでTODOリストを作ってみた
- Go / Gin で超簡単なWebアプリを作る
- GORM - The fantastic ORM library for Golang, aims to be developer friendly.