表題通りGinを使って簡単なサンプルアプリを作ってみたので必要最低限の使い方をまとめてみます。 https://github.com/gin-gonic/gin
準備
- info
- Goのコードは全て一つのmainファイルにまとめることもできたんですが、記事の中でいくつかに分けて埋め込みたかったので分割しました。
- 過剰に分割されているかもしれませんが、スルーでお願いします。
今回の環境は以下の docker-compose.ymlで用意します。
version: '3.7'
services:
gintest:
image: golang:1.13-stretch
container_name: gintest
tty: true
volumes:
- .:/root/
working_dir: /root/
ports:
- "8080:8080"
redis:
image: redis:latest
container_name: gin_redis
tty: true
expose:
- "6379"
networks:
gin_network:
以降の操作はすべて gintest
コンテナ内で行う想定です。
プロンプトは慣例的に \$
に置き換えていますが、コンテナ内の操作ユーザは root
になるので適宜読み替えてください。
パッケージをインストールします。
$ go get -u github.com/gin-gonic/gin
$ go get -u github.com/gin-contrib/sessions
今回はセッションも扱いたいので gin-gonic/contrib
も入れています。
ginでセッション管理するパッケージは以下の2種類があるようです。
https://github.com/gin-gonic/contrib https://github.com/gin-contrib/sessions
前者はあんまりメンテされないみたいなので今回は後者の gin-contrib/sessions
を使います。
最終的に go.mod
はこんな感じになりました
module gintest
go 1.13
require (
github.com/gin-contrib/sessions v0.0.1
github.com/gin-gonic/gin v1.5.0
github.com/go-playground/universal-translator v0.17.0 // indirect
github.com/gorilla/sessions v1.2.0 // indirect
github.com/json-iterator/go v1.1.8 // indirect
github.com/leodido/go-urn v1.2.0 // indirect
github.com/mattn/go-isatty v0.0.10 // indirect
golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e // indirect
gopkg.in/go-playground/validator.v9 v9.30.2 // indirect
gopkg.in/yaml.v2 v2.2.7 // indirect
)
以降、この(go.modが置かれている)ディレクトリは gintest
というパッケージとして扱います。
Usage
使い方はシンプルです。
gin.Default()
か gin.New()
で作った
*Engine
に対して、以下のメソッドを使って パス
と
ハンドラ関数(ビュー)
の紐付けを行って Run()
メソッドでサーバを起動するように記述するだけです。
本来これらは RouterGroup
のメソッドですが、Engine に埋め込まれているため、Engineからも呼び出すことができます。
少し(後述する)無駄な設定が混ざってますが、だいたいこんな感じです
package main
import (
"gintest/debug"
"gintest/users"
"github.com/gin-contrib/sessions"
"github.com/gin-contrib/sessions/redis"
"github.com/gin-gonic/gin"
)
func main() {
r := gin.Default()
store, _ := redis.NewStore(10, "tcp", "redis:6379", "", []byte("secret"))
//store := cookie.NewStore([]byte("secret"))
r.Use(sessions.Sessions("session", store))
r.LoadHTMLGlob("templates/*.tmpl")
r.GET("/", users.Index)
r.GET("/debug", debug.ParamDebug)
r.POST("/debug", debug.ParamDebug)
r.GET("/login", users.LoginForm)
r.POST("/login", users.Login)
r.GET("/logout", users.Logout)
a := r.Group("/")
a.Use(users.AuthRequired)
{
a.GET("/mypage", users.Mypage)
}
r.Run("0.0.0.0:8080")
}
あとは、 main.go
を実行すれば..
$ 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 (7):
-
- debug.tmpl
- header.tmpl
- header
- index.tmpl
- login.tmpl
- mypage.tmpl
[GIN-debug] GET / --> gintest/users.Index (4 handlers)
[GIN-debug] GET /debug --> gintest/debug.ParamDebug (4 handlers)
[GIN-debug] POST /debug --> gintest/debug.ParamDebug (4 handlers)
[GIN-debug] GET /login --> gintest/users.LoginForm (4 handlers)
[GIN-debug] POST /login --> gintest/users.Login (4 handlers)
[GIN-debug] GET /logout --> gintest/users.Logout (4 handlers)
[GIN-debug] GET /mypage --> gintest/users.Mypage (5 handlers)
[GIN-debug] Listening and serving HTTP on 0.0.0.0:8080
サーバが起動しました。あとは 起動したホストにアクセスするだけです。
ここからは各機能について掘り下げていきます。
Handler function
ハンドラ関数は Context のポインタを引数にとり、 HTML や JSON メソッドを使ってレスポンスを返却します。
以下はパラメータを表示する関数です。
package debug
import (
"encoding/json"
"net/http"
"github.com/gin-gonic/gin"
)
func ParamDebug(c *gin.Context) {
q := c.Query("q")
dq := c.DefaultQuery("dq", "default value")
qa := c.QueryArray("qa")
p := c.PostForm("p")
dp := c.DefaultPostForm("dp", "default value")
pa := c.PostFormArray("pa")
j, _ := json.Marshal(map[string]interface{}{
"q": q,
"dq": dq,
"qa": qa,
"p": p,
"dp": dp,
"pa": pa,
})
c.HTML(http.StatusOK, "debug.tmpl", gin.H{
"json": string(j),
})
}
メソッドの第一引数には HTTPステータスコード を指定します。
Context
はパラメータを解析するためのメソッドを持ちます。
- Query
- クエリストリングから指定された特定のパラメータ値を文字列で取得する
- 同じパラメータがあった場合は戦闘のパラメータが返却される
- パラメータがなかった場合、空文字が返却される
- DefaultQuery
- Queryと概ね同じ挙動
- パラメータがなかった場合、第2引数に指定した文字列が返却される
- パラメータに空文字が指定された場合、空文字が返却される
- QueryArray
- クエリストリングから指定されたパラメータ値をすべて文字列のスライスで取得する。
- パラメータがなかった場合、空のスライスが返却される
- PostForm
- リクエストボディから指定されたパラメータ値を文字列で取得する。
- DefaultPostForm
- PostForm と概ね同じ挙動
- パラメータがなかった場合、第2引数に指定した文字列が返却される
- パラメータに空文字が明示された場合、空文字が返却される
- PostFormArray
- リクエストボディから指定されたパラメータ値をすべて文字列のスライスで取得する。
- パラメータがなかった場合、空のスライスが返却される
これらのメソッドを使ってパラメータをJSONで表示すると以下のような感じになります。
Template
Context.HTML
で描画する
テンプレートは特定のディレクトリに配置し
r.LoadHTMLGlob("templates/*.tmpl")
のように登録します。
先程の画面は以下のテンプレートを描画しています。
<html>
<a href="/">TOPにもどる</a>
<pre style="font-size: 20px;">
{{ .json }}
</pre>
<fieldset><legend>GET method で送信する</legend>
<form method="get">
<table>
<tbody>
<tr><th>Query(q):</th><td><input name="q" value="a" /></td></tr>
<tr><th>Query(q):</th><td><input name="q" value="b" /></td></tr>
<tr><th>DefaultQuery(dq):</th><td><input name="dq" value="c" /></td></tr>
<tr><th>DefaultQuery(dq):</th><td><input name="dq" value="d" /></td></tr>
<tr><th>QueryArray(qa):</th><td><input name="qa" value="e" /></td></tr>
<tr><th>QueryArray(qa):</th><td><input name="qa" value="f" /></td></tr>
</tbody>
</table>
<button type="submit">送信</button>
</form>
</fieldset>
<fieldset><legend>POST method で送信する</legend>
<form method="post">
<table>
<tbody>
<tr><th>PostForm(p):</th><td><input name="p" value="g" /></td></tr>
<tr><th>PostForm(p):</th><td><input name="p" value="h" /></td></tr>
<tr><th>DefaultPostForm(dp):</th><td><input name="dp" value="i" /></td></tr>
<tr><th>DefaultPostForm(dp):</th><td><input name="dp" value="j" /></td></tr>
<tr><th>PostFormArray(pa):</th><td><input name="pa" value="k" /></td></tr>
<tr><th>PostFormArray(pa):</th><td><input name="pa" value="l" /></td></tr>
</tbody>
</table>
<button type="submit">送信</button>
</form>
</fieldset>
</html>
{{ define "header" }}
<div style="width: 100%; padding: 10px; box-sizing: border-box; background-color: #bbb;">
{{ if .user.LoggedIn }}
{{ .user.Greeting "Hello" }} <a href="/logout">ログアウトする</a>
{{ else }}
<a href="/login">ログインする</a>
{{ end }}
</div>
{{ end }}
テンプレートの可変部分は {{}}
(波括弧2つ)で囲みます。
数値はそのまま評価され、 .
で始まる場合はコンテキストとして評価されます。
- info
- テンプレートのコンテキストは
gin.H
というマップを使って渡します。
c.HTML(http.StatusOK, "debug.tmpl", gin.H{ "json": string(j), })
- テンプレートのコンテキストは
gin.H
というマップを使って渡します。
それ以外は関数呼び出しになります。 関数はコンテキストとして渡すことはできず SetFuncMap でを登録する必要があります。(今回はやりません)
今回使っている関数は予め登録されている define
と
template
です。 {{ define テンプレート名 }}
で命名し、
{{ template テンプレート名 }}
で取り込みます。
お気付きの通り、関数やメソッドの実行は丸括弧で囲まず、スペースで引数を区切ります。
これらのテンプレート記法は Gin オリジナルというわけではなく、標準パッケージの template に従うので詳しく知りたい場合はそちらを参照してください。
Session
操作中のユーザを特定し、情報を持たせるためにセッション管理をします。
今回は以下のような User
構造体を定義します
package users
import "fmt"
type User struct {
ID int `json: id`
Password string `json: password`
Name string `json: name`
Memo string `json: memo`
}
func (u User) LoggedIn() bool {
return u.ID != 0
}
func (u User) Greeting(word string) string {
return fmt.Sprintf("%s, I am %s.", word, u.Name)
}
- info
- この記事でやることをこれ以上増やしたくないので データストアとして RDBMS は使わず、以下のような JSON ファイルをDB代わりに使います。
-
users.json
[ {"id": 1, "password": "password1", "name": "user name1", "memo": "first user aaaaaaaaaaaaa"}, {"id": 2, "password": "password2", "name": "user name2", "memo": "second user"}, {"id": 3, "password": "password3", "name": "user name3", "memo": "third user"} ]
gin-contrib/session
はセッション管理機能を提供してくれます。
以下のセッションストアを利用できます。
- cookie-based
- セッションに入れたデータすべてをCookieにいれます。
- 入れるデータが大きくなればなるほどCookieが肥大化して通信が圧迫されるので、何でもかんでもCookieに入れるのは考えものです。
- Redis
- Memcached
- MongoDB
ここでは Redis
をセッションストアとして利用します。
README に従って store
を作成し、以下のように与えてあげるだけで利用可能になります。
なお、ここで与えている接続先は記事の先頭に載せた
docker-compose
で作成しています。
store, _ := redis.NewStore(10, "tcp", "redis:6379", "", []byte("secret"))
r.Use(sessions.Sessions("session", store)) // ここに指定した名前のCookieが作られる
- info
Key "github.com/gin-contrib/sessions" does not exist
と言われる場合、セッションミドルウェアを登録し忘れていないか確認してみましょう。 https://github.com/gin-contrib/sessions/issues/40#issuecomment-354376874// code from README r := gin.Default() store := sessions.NewCookieStore([]byte("secret")) r.Use(sessions.Sessions("mysession", store)) // your middleware r.Use(utils.AuthMiddleware())
セッションを利用する各関数で session := sessions.Default(c)
としてユーザに紐づく session
を取得。
sesion.Get(key string)
:
- キーに紐づく値を取得
sesion.Set(key string, value []byte)
:
- キーに値を値を保存
session.Clear()
:
- すべての値を削除
session.Delete(key string)
:
- キーに一致する値を削除
この session
に対して上記の各種操作を行った後に
session.Save()
でセッションストアに反映します。
セッションに紐づく「ユーザを取得」さらにそれを使い「未ログインユーザを弾く」ユーティリティ関数は以下のように書けます。
package users
import (
"encoding/json"
"net/http"
"github.com/gin-contrib/sessions"
"github.com/gin-gonic/gin"
)
func GetUser(c *gin.Context) (user User) {
session := sessions.Default(c)
user = User{}
if v := session.Get(userKey); v != nil {
json.Unmarshal(v.([]byte), &user)
}
return
}
func AuthRequired(c *gin.Context) {
user := GetUser(c)
if !user.LoggedIn() {
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "unauthorized"})
return
}
c.Next()
}
// マイページから未ログインユーザを弾く例
a.Use(users.AuthRequired)
{
a.GET("/mypage", users.Mypage)
}
最後にセッションを使った基本的な画面を書いてみました。前で定義した
GetUser
関数も使っています。
package users
import (
"encoding/json"
"io/ioutil"
"net/http"
"strconv"
"github.com/gin-contrib/sessions"
"github.com/gin-gonic/gin"
)
const userKey = "user"
func Login(c *gin.Context) {
session := sessions.Default(c)
binary, _ := ioutil.ReadFile("./users.json")
users := make([]User, 0)
json.Unmarshal(binary, &users)
for _, u := range users {
if strconv.Itoa(u.ID) == c.PostForm("id") && u.Password == c.PostForm("password") {
text, _ := json.Marshal(u)
session.Set(userKey, text)
if err := session.Save(); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to save session"})
return
}
c.Redirect(http.StatusFound, "/mypage")
return
}
}
c.HTML(http.StatusUnauthorized, "login.tmpl", gin.H{
"errorMessage": "ログイン失敗",
})
}
func Logout(c *gin.Context) {
session := sessions.Default(c)
session.Delete(userKey)
if err := session.Save(); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to save session"})
return
}
c.Redirect(http.StatusFound, "/")
}
func Mypage(c *gin.Context) {
user := GetUser(c)
c.HTML(http.StatusOK, "mypage.tmpl", gin.H{
"user": user,
})
}
func Index(c *gin.Context) {
user := GetUser(c)
c.HTML(http.StatusOK, "index.tmpl", gin.H{
"title": "Sample",
"user": user,
})
}
func LoginForm(c *gin.Context) {
c.HTML(http.StatusOK, "login.tmpl", gin.H{})
}
ログイン後に Redis を覗いてみると登録できていますね。よかった。
# redis-cli
127.0.0.1:6379> keys *
1) "session_BP3G7FZGYKDPXM7XKUJYO3Q2BN2ZD75ZLWNS4RCNRTQCUCJLS54Q"
127.0.0.1:6379> get session_BP3G7FZGYKDPXM7XKUJYO3Q2BN2ZD75ZLWNS4RCNRTQCUCJLS54Q
"\x0e\xff\x81\x04\x01\x02\xff\x82\x00\x01\x10\x01\x10\x00\x00g\xff\x82\x00\x01\x06string\x0c\x06\x00\x04user\a[]uint8\nJ\x00H{\"ID\":2,\"Password\":\"password2\",\"Name\":\"user name2\",\"Memo\":\"second user\"}"
今回のようにデータベースに入っているような恒久的なデータをセッションに入れるときは、更新時にセッションデータも更新することを忘れないようにしましょう。
ひとまずDBアクセスを除き最低限の機能は触れたと思います。
シンプルで好感が持てるフレームワークですね。 全部入りで何でもかんでもやってくれるフレームワークよりも学習コストは低いと思います。
またなにかあったら記事にします。
参考