Help us understand the problem. What is going on with this article?

Go / Gin で超簡単なWebアプリを作る

はじめに

Go言語と、Go向けWebフレームワークの中でも有名なGinを用いて、CRUDを実装した簡単なtodoアプリをローカルに作りたいと思います。

筆者の使ったことのある言語としては、他にはPython(flask/django)のみで、webアプリ開発も初めて数ヶ月程度といった状態で、Goは少し前に触り始めたという次第です。

それと、埋めて欲しいとの要望で、去年のアドベントカレンダーに今更登録してます。ややこしくて申し訳ないです。

注意

筆者はGoどころかwebアプリ開発も非常に未熟なため、酷いコード等が多々見られると思われます。ご了承ください。
また非常に初歩的な内容な上に、間違いもあるかもしれないので、その際は教えていただけると幸いです。

環境

  • OS: Mac(Mojave 10.14.3)
  • Go: 1.12.1
  • エディタ: GoLand
  • DB: sqlite3
  • パッケージ管理:Go Modules

Go Modulesについては説明を省かせていただきます。

とりあえずやってみる

Hello World

まずはGinの用意をして

go get github.com/gin-gonic/gin

とりあえずHello World でもブラウザに出してみましょう。

main.go
package main

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

func main() {
    router := gin.Default()
    router.LoadHTMLGlob("templates/*.html")

    router.GET("/", func(ctx *gin.Context){
        ctx.HTML(200, "index.html", gin.H{})
    })

    router.Run()
}
templates/index.html
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <title>Sample App</title>
</head>
<body>
    <h1>Hello World!!!</h1>
</body>
</html>

終わりです。簡単。

では実行して、localhost:8080にアクセスしてみます。

go run main.go

スクリーンショット 2019-03-17 18.22.17.png
index.htmlがそのまま表示されましたね。
router.LoadHTMLGlobでHTMLを読み込むディレクトリを指定したあと、router.GETでindex.htmlにGETで繋いでいます。勿論、ここのGETをPOSTに変えることで、POSTを送ることもできます。

HTMLに値を渡す

先ほどはHTMLをそのまま返したので今度はgo側の変数をHTMLに渡し、表示してみます。

main.go
data := "Hello Go/Gin!!"

router.GET("/", func(ctx *gin.Context){
    ctx.HTML(200, "index.html", gin.H{"data": data})
})
index.html
<!-- <h1>Hello World!!!</h1> -->
<h1>{{.data}}</h1>

少々手を加えてみました。
まずmain.goの方は、router.GET()内のgin.H{}にmap(Python等でいう辞書型)で値を渡しています。それをHTML側では、変数名の頭に.をつけることで、HTML上に表示することができます。実際に実行してみると、
スクリーンショット 2019-03-17 18.40.17.png
go側で定義した文字列が表示されています。これでGoとHTMLの繋げ方は簡単にわかったと思います。

データベース

実際にtodoアプリを作るとなればデータベースが必要です。なので、今回はsqlite3とGORMを用いてデータの操作を行いたいと思います。インストールは以下の通りに。

go get github.com/jinzhu/gorm
go get github.com/mattn/go-sqlite3

GORM

今回参考にさせていただいた、GORMについて情報をまとめてくださっている方々がいらっしゃるので、紹介したいと思います。

また、こちらの記事が、幅広くGORMについて書かれており、オススメです。

import

まずパッケージのimportですが、sqlite3やmysqlなどのDBのパッケージですが、操作はGORMで行うため、importはしますが以下のように使わないということを明示します。

main.go
import (
    "github.com/gin-gonic/gin"
    "github.com/jinzhu/gorm"

    _ "github.com/mattn/go-sqlite3"
)

直接的にimportされてない!などとは言われませんが、ちゃんとimportの中にsqlite3を入れておかないとエラーが起きてしまうので、忘れないでください。私は忘れて困ってました

DB操作(全体)

では実際にデータベースの操作です。
まず全体を紹介すると、

main.go
type Todo struct {
    gorm.Model
    Text   string
    Status string
}

//DB初期化
func dbInit() {
    db, err := gorm.Open("sqlite3", "test.sqlite3")
    if err != nil {
        panic("データベース開けず!(dbInit)")
    }
    db.AutoMigrate(&Todo{})
    defer db.Close()
}

//DB追加
func dbInsert(text string, status string) {
    db, err := gorm.Open("sqlite3", "test.sqlite3")
    if err != nil {
        panic("データベース開けず!(dbInsert)")
    }
    db.Create(&Todo{Text: text, Status: status})
    defer db.Close()
}

//DB更新
func dbUpdate(id int, text string, status string) {
    db, err := gorm.Open("sqlite3", "test.sqlite3")
    if err != nil {
        panic("データベース開けず!(dbUpdate)")
    }
    var todo Todo
    db.First(&todo, id)
    todo.Text = text
    todo.Status = status
    db.Save(&todo)
    db.Close()
}

//DB削除
func dbDelete(id int) {
    db, err := gorm.Open("sqlite3", "test.sqlite3")
    if err != nil {
        panic("データベース開けず!(dbDelete)")
    }
    var todo Todo
    db.First(&todo, id)
    db.Delete(&todo)
    db.Close()
}

//DB全取得
func dbGetAll() []Todo {
    db, err := gorm.Open("sqlite3", "test.sqlite3")
    if err != nil {
        panic("データベース開けず!(dbGetAll())")
    }
    var todos []Todo
    db.Order("created_at desc").Find(&todos)
    db.Close()
    return todos
}

//DB一つ取得
func dbGetOne(id int) Todo {
    db, err := gorm.Open("sqlite3", "test.sqlite3")
    if err != nil {
        panic("データベース開けず!(dbGetOne())")
    }
    var todo Todo
    db.First(&todo, id)
    db.Close()
    return todo
}

と、なっています。
では一つずつ見ていきます。

モデル設計

main.go
type Todo struct {
    gorm.Model
    Text   string
    Status string
}

これはモデルの定義です。gorm.Modelで標準のモデルを呼び出し、そこにTextとStatusを追加しています。
すると、

  • テーブル名:todos
  • カラム
    • id
    • created_at
    • updated_at
    • deleted_at
    • text(追加)
    • status(追加)

このようなテーブル設計になります。この時、テーブル名が自動で複数形になるため、構造体の名前は単数形に。各カラム名は頭文字を大文字にします。

今回は標準のモデル設定を呼び出しましたが、勿論これを使わずに自分で設定することも可能です。それらについては先ほど紹介した記事等を参考にしてみてください。

マイグレート

次にマイグレートです。

main.go
//DBマイグレート
func dbInit() {
    db, err := gorm.Open("sqlite3", "test.sqlite3")
    if err != nil {
        panic("データベース開けず!(dbInit)")
    }
    db.AutoMigrate(&Todo{})
    defer db.Close()
}

まず、gorm.Open()の第1引数で、使用するDBのデバイス。第2引数にファイル名を設定します。そして、db.AutoMigrate()でマイグレートを実行します。ここでは、ファイルが無ければ生成を行い、すでにファイルがありマイグレートも行われていれば何も行いません。

最後にdb.Close()で開いたDBを閉じます。この処理はDBを使う全ての操作で行なっています。

CREATE(INSERT)

main.go
//DB追加
func dbInsert(text string, status string) {
    db, err := gorm.Open("sqlite3", "test.sqlite3")
    if err != nil {
        panic("データベース開けず!(dbInsert)")
    }
    db.Create(&Todo{Text: text, Status: status})
    defer db.Close()
}

ほとんどマイグレートと同じですね。
Todoという構造体に与えられた引数をいれた状態で、db.Create()に渡しています。

READ(SELECT)

読み込みは二つのパターンを用意してます。

main.go
//DB全取得
func dbGetAll() []Todo {
    db, err := gorm.Open("sqlite3", "test.sqlite3")
    if err != nil {
        panic("データベース開けず!(dbGetAll())")
    }
    var todos []Todo
    db.Order("created_at desc").Find(&todos)
    db.Close()
    return todos
}

//DB一つ取得
func dbGetOne(id int) Todo {
    db, err := gorm.Open("sqlite3", "test.sqlite3")
    if err != nil {
        panic("データベース開けず!(dbGetOne())")
    }
    var todo Todo
    db.First(&todo, id)
    db.Close()
    return todo
}

まずは全取得です。db.Find(&todos)で構造体Todoに対するテーブルの要素全てを取得し、それをOrder("created_at desc)で新しいものが上に来るよう並び替えを行なっています。

次に、検索した1件の取得です。こちらは全取得と同様Firstを用いるのですが、第2引数にはidを加えることで特定のレコードを取得することができます。

また、読み込む時はvar todo Todoで先に変数を定義するようにしています。

UPDATE

main.go
//DB更新
func dbUpdate(id int, text string, status string) {
    db, err := gorm.Open("sqlite3", "test.sqlite3")
    if err != nil {
        panic("データベース開けず!(dbUpdate)")
    }
    var todo Todo
    db.First(&todo, id)
    todo.Text = text
    todo.Status = status
    db.Save(&todo)
    db.Close()
}

UPDATEは読みこんでから上書きするので少しコードが増えてますね。
まず1件取得と同様に特定のレコードを呼び出します。それをtodoに入れたあと、そのtodoの各要素(各カラム)に引数で与えられたデータを入れています。ここで各カラムの呼び出しをtodo.Hogeとしていますが、これは構造体を読んでいるため、頭文字が大文字となっています。
ならば、GORMの標準のモデルはどういう名前で入っているかというと、

  • id → ID
  • created_at → CreatedAt
  • updated_at → UpdatedAt
  • deleted_at → DeletedAt

となっています。これらはHTMLにGO側から変数を渡した時の呼び出すときにも使うので注意してください。

DELETE

main.go
//DB削除
func dbDelete(id int) {
    db, err := gorm.Open("sqlite3", "test.sqlite3")
    if err != nil {
        panic("データベース開けず!(dbDelete)")
    }
    var todo Todo
    db.First(&todo, id)
    db.Delete(&todo)
    db.Close()
}

まずはDELETEもほか同様にdb.First()でレコードを取得したあと、db.Delete(%todo)で取得したレコードを削除しています。

機能実装していく

main.go
import (
    "strconv"

    "github.com/gin-gonic/gin"
    "github.com/jinzhu/gorm"
    _ "github.com/mattn/go-sqlite3"
)

まず、strconvを追加し、

main.go
func main() {
    router := gin.Default()
    router.LoadHTMLGlob("templates/*.html")

    dbInit()

    //Index
    router.GET("/", func(ctx *gin.Context) {
        todos := dbGetAll()
        ctx.HTML(200, "index.html", gin.H{
            "todos": todos,
        })
    })

    //Create
    router.POST("/new", func(ctx *gin.Context) {
        text := ctx.PostForm("text")
        status := ctx.PostForm("status")
        dbInsert(text, status)
        ctx.Redirect(302, "/")
    })

    //Detail
    router.GET("/detail/:id", func(ctx *gin.Context) {
        n := ctx.Param("id")
        id, err := strconv.Atoi(n)
        if err != nil {
            panic(err)
        }
        todo := dbGetOne(id)
        ctx.HTML(200, "detail.html", gin.H{"todo": todo})
    })

    //Update
    router.POST("/update/:id", func(ctx *gin.Context) {
        n := ctx.Param("id")
        id, err := strconv.Atoi(n)
        if err != nil {
            panic("ERROR")
        }
        text := ctx.PostForm("text")
        status := ctx.PostForm("status")
        dbUpdate(id, text, status)
        ctx.Redirect(302, "/")
    })

    //削除確認
    router.GET("/delete_check/:id", func(ctx *gin.Context) {
        n := ctx.Param("id")
        id, err := strconv.Atoi(n)
        if err != nil {
            panic("ERROR")
        }
        todo := dbGetOne(id)
        ctx.HTML(200, "delete.html", gin.H{"todo": todo})
    })

    //Delete
    router.POST("/delete/:id", func(ctx *gin.Context) {
        n := ctx.Param("id")
        id, err := strconv.Atoi(n)
        if err != nil {
            panic("ERROR")
        }
        dbDelete(id)
        ctx.Redirect(302, "/")

    })

    router.Run()
}

index.html
<body>
<h2>追加</h2>
<form method="post" action="/new">
    <p>内容<input type="text" name="text" size="30" placeholder="入力してください" ></p>
    <p>状態
        <select name="status">
            <option value="未実行">未実行</option>
            <option value="実行中">実行中</option>
            <option value="終了">終了</option>
        </select>
    </p>
    <p><input type="submit" value="Send"></p>
</form>

<ul>
    {{ range .todos }}
        <li>内容:{{ .Text }}、状態:{{ .Status }}
            <label><a href="/detail/{{.ID}}">編集</a></label>
            <label><a href="/delete_check/{{.ID}}">削除</a></label>
        </li>
    {{end}}
</ul>
</body>
detail.html
<body>
<h2>aaa</h2>

<p>{{.user.ID}}</p>
<p>{{.user.Name}}</p>
<p>{{.user.Age}}</p>

<form method="post" action="/update/{{.todo.ID}}">
    <p>内容<input type="text" name="text" size="30" value="{{.todo.Text}}" ></p>
    <p>状態
        <select name="status">
            {{if eq .todo.Status "未実行"}}
                <option value="未実行" selected>未実行</option>
                <option value="実行中">実行中</option>
                <option value="終了">終了</option>
            {{else if eq .todo.Status "実行中"}}
                <option value="未実行">未実行</option>
                <option value="実行中" selected>実行中</option>
                <option value="終了">終了</option>
            {{else}}
                <option value="未実行">未実行</option>
                <option value="実行中">実行中</option>
                <option value="終了" selected>終了</option>
            {{end}}
        </select>
    </p>
    <p><input type="submit" value="Send"></p>
</form>
</body>
delete.html
<body>
<h1>削除確認</h1>
<p>本当に削除しますか?</p>
<ul>
    <li>内容: {{.todo.Text}}</li>
    <li>状態: {{.todo.Status}}</li>
    <li>作成時間: {{.todo.CreatedAt}}</li>
</ul>

<form method="post" action="/delete/{{.todo.ID}}">
    <p><input type="submit" value="削除"></p>
    <p><a href="/">戻る</a></p>
</form>
</body>

基本的なことは前の二つにやったことを組み合わせているだけです。なので、それ以外のポイントをいくつか紹介します。

POST

main.go
    //Create
    router.POST("/new", func(ctx *gin.Context) {
        text := ctx.PostForm("text")
        status := ctx.PostForm("status")
        dbInsert(text, status)
        ctx.Redirect(302, "/")
    })

これはrouter.POSTとなっているため、POSTのリクエストを送っています。
ctx.Redirect(302, "/")は、その名の通り、localhost:8080/にステータスコード302としてリダイレクトをしています。

個別ページ

続いて詳細ページですが、

main.go
    //Detail
    router.GET("/detail/:id", func(ctx *gin.Context) {
        n := ctx.Param("id")
        id, err := strconv.Atoi(n)
        if err != nil {
            panic(err)
        }
        todo := dbGetOne(id)
        ctx.HTML(200, "detail.html", gin.H{"todo": todo})
    })

ここで、リクエスト先URLが:idというところがあります。これは例えば/detail/2というURLにリクエストされた場合、idというパラメータに2という値が文字列として入れられた状態でアクセスされます。
なので、その後

main.go
n := ctx.Param("id")
id, err := strconv.Atoi(n)

ここでそのidの値を受け取り、int型に変換しています。

HTML

またHTML側でのGoの埋め込み型ですが、

index.html
    {{ range .todos }}
        <li>内容:{{ .Text }}、状態:{{ .Status }}
            <label><a href="/detail/{{.ID}}">編集</a></label>
            <label><a href="/delete_check/{{.ID}}">削除</a></label>
        </li>
    {{end}}

ここではtodosに、配列として複数のレコードが格納されているため、rangeを用いて1レコードずつ表示させています。
また、<a href="/detail/{{.ID}}">では、文字列の中に変数.IDを埋め込んでいます。

index.html
<form method="post" action="/new">
    <p>内容<input type="text" name="text" size="30" placeholder="入力してください" ></p>
    <p>状態
        <select name="status">
            <option value="未実行">未実行</option>
            <option value="実行中">実行中</option>
            <option value="終了">終了</option>
        </select>
    </p>
    <p><input type="submit" value="Send"></p>
</form>

ここではまず1行目の<form method="post" action="/new">とすることで、Go側の、

main.go
    //Create
    router.POST("/new", func(ctx *gin.Context) {
        text := ctx.PostForm("text")
        status := ctx.PostForm("status")
        dbInsert(text, status)
        ctx.Redirect(302, "/")
    })

ここに繋がっています。

次に、ここではif文で与えられたStatusがどの状態か、で場合分けを行なっています。

detail.html
<select name="status">
    {{if eq .todo.Status "未実行"}}
        <option value="未実行" selected>未実行</option>
        <option value="実行中">実行中</option>
        <option value="終了">終了</option>
    {{else if eq .todo.Status "実行中"}}
        <option value="未実行">未実行</option>
        <option value="実行中" selected>実行中</option>
        <option value="終了">終了</option>
    {{else}}
        <option value="未実行">未実行</option>
        <option value="実行中">実行中</option>
        <option value="終了" selected>終了</option>
    {{end}}
</select>

この時、{{if eq .todo.Status "未実行"}}となっていますが、これはif .todo.Status == "未実行"ということを表現しています。
その他、HTML上での評価の書き方は、こちらを参考にしてください。

完成

それでは実行をしてみます

go run main.go

localhost:8080にアクセスしてみると・・・
GoTodo説明.gif
このような動作をするのではないかと思います。

まとめ

Go/Gin はまだ多くの人が使っておらず、日本語記事も少ないから難しい。というイメージを持っていました。ですが、こういった簡単なことなら思ったよりすぐに出来ました。
今後は、ログイン機能や複数モデルを用いたリレーションなどを用い、しっかりとしたwebアプリを作るのにチャレンジしようと思います。

参考URL

hyo_07
大学院にてブロックチェーンの研究を行っています
https://github.com/hyo07
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away