go言語+beegoで掲示板を作る

前回の続きでgo言語用のwebフルスタックフレームワークである beego で実際にサービスを作ってみようと思います。前回はコード自動生成機能を試してみて CURD するためのコードを作成しました。何を作るかですがとりあえず掲示板みたいなものを作ろうかと思います。

ユースケースとしてはユーザがフォームから文字列を送信するとDBに文字列が保存され、その文字列を表示ができる。という具合で進めようと思いつつ、beego の機能を触って見ていこうと思います。

書き込みページの作成

scaffold を使っての view は自動作成してくれなかったので自前で HTML を書いていこうと思います。掲示板ということなのでまずは書き込みのページを作っていこうと思います。トップページから掲示板へ遷移して、フォームが見えるというころまでを行おうと思います。

bootstrap 導入

webサイト作りに個人的に欠かせないのは CSSフレームワークである bootstrap
こいつを導入していこうと思います。
まずは共通で読み込みのできる layout.html を作っていきます。
公式のドキュメントはこのへん
サンプルにはすでに bootstrap3 が入っていましたが、せっかくなので bootstrap4 を入れてみようと思います。
views/layout/layout.html 作成

<span class="hljs-meta"><!DOCTYPE html></span>
<span class="hljs-tag"><<span class="hljs-name">html</span>></span>
<span class="hljs-tag"><<span class="hljs-name">head</span>></span>
    <span class="hljs-tag"><<span class="hljs-name">title</span>></span>haruch<span class="hljs-tag"></<span class="hljs-name">title</span>></span>
    <span class="hljs-tag"><<span class="hljs-name">meta</span> <span class="hljs-attr">name</span>=<span class="hljs-string">"viewport"</span> <span class="hljs-attr">content</span>=<span class="hljs-string">"width=device-width, initial-scale=1.0"</span>></span>
    <span class="hljs-tag"><<span class="hljs-name">meta</span> <span class="hljs-attr">http-equiv</span>=<span class="hljs-string">"Content-Type"</span> <span class="hljs-attr">content</span>=<span class="hljs-string">"text/html; charset=utf-8"</span>></span>
    <span class="hljs-tag"><<span class="hljs-name">link</span> <span class="hljs-attr">rel</span>=<span class="hljs-string">"stylesheet"</span> <span class="hljs-attr">href</span>=<span class="hljs-string">"https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/css/bootstrap.min.css"</span> <span class="hljs-attr">integrity</span>=<span class="hljs-string">"sha384-MCw98/SFnGE8fJT3GXwEOngsV7Zt27NXFoaoApmYm81iuXoPkFOJwJ8ERdknLPMO"</span> <span class="hljs-attr">crossorigin</span>=<span class="hljs-string">"anonymous"</span>></span>
    {{.HtmlHead}}
<span class="hljs-tag"></<span class="hljs-name">head</span>></span>
<span class="hljs-tag"><<span class="hljs-name">body</span>></span>

    <span class="hljs-tag"><<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"container"</span>></span>
        {{.LayoutContent}}
    <span class="hljs-tag"></<span class="hljs-name">div</span>></span>
    <span class="hljs-tag"><<span class="hljs-name">div</span>></span>
        {{.SideBar}}
    <span class="hljs-tag"></<span class="hljs-name">div</span>></span>
    <span class="hljs-tag"><<span class="hljs-name">script</span> <span class="hljs-attr">src</span>=<span class="hljs-string">"https://code.jquery.com/jquery-3.3.1.slim.min.js"</span> <span class="hljs-attr">integrity</span>=<span class="hljs-string">"sha384-q8i/X+965DzO0rT7abK41JStQIAqVgRVzpbzo5smXKp4YfRvH+8abtTE1Pi6jizo"</span> <span class="hljs-attr">crossorigin</span>=<span class="hljs-string">"anonymous"</span>></span><span class="hljs-tag"></<span class="hljs-name">script</span>></span>
    <span class="hljs-tag"><<span class="hljs-name">script</span> <span class="hljs-attr">src</span>=<span class="hljs-string">"https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.3/umd/popper.min.js"</span> <span class="hljs-attr">integrity</span>=<span class="hljs-string">"sha384-ZMP7rVo3mIykV+2+9J3UJ46jBk0WLaUAdn689aCwoqbBJiSnjAK/l8WvCWPIPm49"</span> <span class="hljs-attr">crossorigin</span>=<span class="hljs-string">"anonymous"</span>></span><span class="hljs-tag"></<span class="hljs-name">script</span>></span>
    <span class="hljs-tag"><<span class="hljs-name">script</span> <span class="hljs-attr">src</span>=<span class="hljs-string">"https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/js/bootstrap.min.js"</span> <span class="hljs-attr">integrity</span>=<span class="hljs-string">"sha384-ChfqqxuZUCnJSK3+MXmPNIyE6ZbWh2IMqE241rYiqJxyMiZ6OW/JmZQ5stwEULTy"</span> <span class="hljs-attr">crossorigin</span>=<span class="hljs-string">"anonymous"</span>></span><span class="hljs-tag"></<span class="hljs-name">script</span>></span>
    {{.Scripts}}
<span class="hljs-tag"></<span class="hljs-name">body</span>></span>
<span class="hljs-tag"></<span class="hljs-name">html</span>></span>

そして views/index.tpl に適当に bootstrap のクラスを書き込み動作テストです。

<span class="hljs-tag"><<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"alert alert-primary"</span> <span class="hljs-attr">role</span>=<span class="hljs-string">"alert"</span>></span>
  bootstrap 入ったよ!
<span class="hljs-tag"></<span class="hljs-name">div</span>></span>

特に意味なくアラートですがちゃんと表示されました。

書き込みページへの遷移

遷移させるには router の機能を使い URL と controller と view をマッピングしていきます。
一旦HTML を表示させるだけにしようと思うので controller の scaffold で自動生成されたコードは無視していこうと思います。

  • router.go の編集
    新しく

    beego.Include(&controllers.PostController{})

こちらを init() メソッドに追加します。
これを追加することで scaffold で作成された controller に指定した URL でアクセスできるようになります。

  • controller/post.go の編集
    index メソッドを生やし、 scaffold で作成された空の post/index.tpl を紐付けます。
<span class="hljs-comment">// Index ...</span>
<span class="hljs-comment">// @Title Index</span>
<span class="hljs-comment">// @Description show BBS</span>
<span class="hljs-comment">// @Success 200</span>
<span class="hljs-comment">// @Failure 403 body is empty</span>
<span class="hljs-comment">// @router /post/index [get]</span>
<span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-params">(c *PostController)</span> <span class="hljs-title">Index</span><span class="hljs-params">()</span></span> {
    c.TplName = <span class="hljs-string">"post/index.tpl"</span>
}

そしてアノテーションに書かれている /post/index へアクセスすると何もないページへアクセスできることがわかります。

フォームのコーディング

bootstrap を導入したのでサクッとフォームを作っていきます。
ここはもう適当に bootstrap の公式から example を持ってきます。

<span class="hljs-tag"><<span class="hljs-name">form</span>></span>
  <span class="hljs-tag"><<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"form-group"</span>></span>
    <span class="hljs-tag"><<span class="hljs-name">label</span>></span>タイトル<span class="hljs-tag"></<span class="hljs-name">label</span>></span>
    <span class="hljs-tag"><<span class="hljs-name">input</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"form-control"</span> <span class="hljs-attr">placeholder</span>=<span class="hljs-string">"タイトル"</span>></span>
  <span class="hljs-tag"></<span class="hljs-name">div</span>></span>
  <span class="hljs-tag"><<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"form-group"</span>></span>
    <span class="hljs-tag"><<span class="hljs-name">label</span>></span>内容<span class="hljs-tag"></<span class="hljs-name">label</span>></span>
    <span class="hljs-tag"><<span class="hljs-name">textarea</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"form-control"</span> <span class="hljs-attr">rows</span>=<span class="hljs-string">"3"</span> <span class="hljs-attr">placeholder</span>=<span class="hljs-string">"書き込む内容"</span>></span><span class="hljs-tag"></<span class="hljs-name">textarea</span>></span>
  <span class="hljs-tag"></<span class="hljs-name">div</span>></span>
  <span class="hljs-tag"><<span class="hljs-name">button</span> <span class="hljs-attr">type</span>=<span class="hljs-string">"submit"</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"btn btn-primary"</span>></span>送信<span class="hljs-tag"></<span class="hljs-name">button</span>></span>
<span class="hljs-tag"></<span class="hljs-name">form</span>></span>

bootstrapを適用するために post.goindex() メソッドに c.Layout も忘れずに追加してあげます。

<span class="hljs-comment">// Index ...</span>
<span class="hljs-comment">// @Title Index</span>
<span class="hljs-comment">// @Description show BBS</span>
<span class="hljs-comment">// @Success 200</span>
<span class="hljs-comment">// @Failure 403 body is empty</span>
<span class="hljs-comment">// @router /post/index [get]</span>
<span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-params">(c *PostController)</span> <span class="hljs-title">Index</span><span class="hljs-params">()</span></span> {
    c.Layout = <span class="hljs-string">"layout/layout.html"</span>
    c.TplName = <span class="hljs-string">"post/index.tpl"</span>
}

サクッと作れます。

DBへの接続設定追加

正直なんでこのタイミングでってやってて思うんですが、 scaffold でコードの自動生成しただけでば DB の接続設定などは一切やってくれないので DB に対しての操作をしようとすると下記のようなエラーが出て、アプリが落ちると思います。

must have one register DataBase alias named `default`

ってことなのでマニュアルに従って設定していきます。
main.go に下記追加です。

<span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">init</span><span class="hljs-params">()</span></span> {
    orm.RegisterDriver(<span class="hljs-string">"postgres"</span>, orm.DRPostgres)
    orm.RegisterDataBase(
        <span class="hljs-string">"default"</span>,
        <span class="hljs-string">"postgres"</span>,
        <span class="hljs-string">"user=user password=pass host=127.0.0.1 port=5432 dbname=postgres sslmode=disable"</span>)

    orm.RunSyncdb(<span class="hljs-string">"default"</span>, <span class="hljs-literal">false</span>, <span class="hljs-literal">true</span>)
}

フォーム入力値をバックエンドへの送信

DB への設定を書いたところで実際に作ったフォームから ORM を利用して DB に値を保存してみようと思います。
まず View のほうの編集をします。
form タグに actuon と method の追加と、inpot と textarea に name を追加しました。

<span class="hljs-tag"><<span class="hljs-name">form</span> <span class="hljs-attr">action</span>=<span class="hljs-string">"/post"</span> <span class="hljs-attr">method</span>=<span class="hljs-string">"post"</span>></span>
  <span class="hljs-tag"><<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"form-group"</span>></span>
    <span class="hljs-tag"><<span class="hljs-name">label</span>></span>タイトル<span class="hljs-tag"></<span class="hljs-name">label</span>></span>
    <span class="hljs-tag"><<span class="hljs-name">input</span> <span class="hljs-attr">name</span>=<span class="hljs-string">"title"</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"form-control"</span> <span class="hljs-attr">placeholder</span>=<span class="hljs-string">"タイトル"</span>></span>
  <span class="hljs-tag"></<span class="hljs-name">div</span>></span>
  <span class="hljs-tag"><<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"form-group"</span>></span>
    <span class="hljs-tag"><<span class="hljs-name">label</span>></span>内容<span class="hljs-tag"></<span class="hljs-name">label</span>></span>
    <span class="hljs-tag"><<span class="hljs-name">textarea</span> <span class="hljs-attr">name</span>=<span class="hljs-string">"body"</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"form-control"</span> <span class="hljs-attr">rows</span>=<span class="hljs-string">"3"</span> <span class="hljs-attr">placeholder</span>=<span class="hljs-string">"書き込む内容"</span>></span><span class="hljs-tag"></<span class="hljs-name">textarea</span>></span>
  <span class="hljs-tag"></<span class="hljs-name">div</span>></span>
  <span class="hljs-tag"><<span class="hljs-name">button</span> <span class="hljs-attr">type</span>=<span class="hljs-string">"submit"</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"btn btn-primary"</span>></span>送信<span class="hljs-tag"></<span class="hljs-name">button</span>></span>
<span class="hljs-tag"></<span class="hljs-name">form</span>></span>

Controllerのほうは Post() メソッドの @router が ただの / になっていたので下記のように直すのと、そもそも scaffold で生成されたコードは JSON でデータが送られてくることが前提だったみたいなので、POST でパラメータがそのまま送られてきたのを受け取るように修正します。さらに、JSONを返されても困るので、そのまま掲示板へとリダイレクトするように一旦設定しました。

<span class="hljs-comment">// Post ...</span>
<span class="hljs-comment">// @Title Post</span>
<span class="hljs-comment">// @Description create Post</span>
<span class="hljs-comment">// @Param    body        body    models.Post true        "body for Post content"</span>
<span class="hljs-comment">// @Success 201 {int} models.Post</span>
<span class="hljs-comment">// @Failure 403 body is empty</span>
<span class="hljs-comment">// @router /post [post]</span>
<span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-params">(c *PostController)</span> <span class="hljs-title">Post</span><span class="hljs-params">()</span></span> {
    <span class="hljs-keyword">var</span> post models.Post
    <span class="hljs-comment">// データ取得して post オブジェクト作成</span>
    post = models.Post{
        Title: c.GetString(<span class="hljs-string">"title"</span>),
        Body:  c.GetString(<span class="hljs-string">"body"</span>),
    }
    <span class="hljs-comment">// データ保存</span>
    <span class="hljs-keyword">if</span> _, err := models.AddPost(&post); err == <span class="hljs-literal">nil</span> {
        c.Ctx.Output.SetStatus(<span class="hljs-number">201</span>)
        <span class="hljs-comment">// 一応 json 格納しておく</span>
        c.Data[<span class="hljs-string">"json"</span>] = post
        <span class="hljs-comment">// 成功したら掲示板トップにリダイレクト</span>
        c.Redirect(<span class="hljs-string">"/post/index"</span>, <span class="hljs-number">302</span>)
    } <span class="hljs-keyword">else</span> {
        c.Data[<span class="hljs-string">"json"</span>] = err.Error()
        <span class="hljs-comment">// 失敗時は一旦エラーをそのまま描画することにする</span>
        c.ServeJSON()
    }
}

これで書き込み画面からタイトルと内容を入力して送信すると DB に値が保存されるようになります。

DBから書き込みデータをViewに表示

今のままでは一方的にDBに書き込むだけで書き込みが閲覧ができません。
当然ですが掲示板サービスなので書き込みが web 上で閲覧できなくては意味がありません。
自動生成された GetAll()メソッドを参考に書かれた内容の取得と view への表示を実装していきたいと思います。

一旦取得時の検索パラメータなどは無視して GetAll() からの内容をほぼコピペし、最新10件を取得して Viewに渡そうと思います。

<span class="hljs-comment">// Index ...</span>
<span class="hljs-comment">// @Title Index</span>
<span class="hljs-comment">// @Description show BBS</span>
<span class="hljs-comment">// @Success 200</span>
<span class="hljs-comment">// @Failure 403 body is empty</span>
<span class="hljs-comment">// @router /post/index [get]</span>
<span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-params">(c *PostController)</span> <span class="hljs-title">Index</span><span class="hljs-params">()</span></span> {
    c.Layout = <span class="hljs-string">"layout/layout.html"</span>
    c.TplName = <span class="hljs-string">"post/index.tpl"</span>

    <span class="hljs-keyword">var</span> fields []<span class="hljs-keyword">string</span>
    <span class="hljs-keyword">var</span> sortby []<span class="hljs-keyword">string</span>
    <span class="hljs-keyword">var</span> order []<span class="hljs-keyword">string</span>
    <span class="hljs-keyword">var</span> query = <span class="hljs-built_in">make</span>(<span class="hljs-keyword">map</span>[<span class="hljs-keyword">string</span>]<span class="hljs-keyword">string</span>)
    <span class="hljs-keyword">var</span> limit <span class="hljs-keyword">int64</span> = <span class="hljs-number">10</span>
    <span class="hljs-keyword">var</span> offset <span class="hljs-keyword">int64</span>

    l, err := models.GetAllPost(query, fields, sortby, order, offset, limit)

    <span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> {
        c.Data[<span class="hljs-string">"json"</span>] = err.Error()
        c.ServeJSON()
    } <span class="hljs-keyword">else</span> {
        c.Data[<span class="hljs-string">"posts"</span>] = l
    }
}

これで View には posts という名前で DB から取得した値が渡ったのではとは HTML を編集していきます。

<span class="hljs-tag"><<span class="hljs-name">hr</span>></span>

{{range $val := .posts}}
  <span class="hljs-tag"><<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"card bg-light mb-3"</span>></span>
    <span class="hljs-tag"><<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"card-header"</span>></span>{{$val.Id}}. ななしさん<span class="hljs-tag"></<span class="hljs-name">div</span>></span>
    <span class="hljs-tag"><<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"card-body"</span>></span>
      <span class="hljs-tag"><<span class="hljs-name">h5</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"card-title"</span>></span>{{$val.Title}} <span class="hljs-tag"></<span class="hljs-name">h5</span>></span>
      <span class="hljs-tag"><<span class="hljs-name">p</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"card-text"</span>></span>{{$val.Body}}<span class="hljs-tag"></<span class="hljs-name">p</span>></span>
    <span class="hljs-tag"></<span class="hljs-name">div</span>></span>
  <span class="hljs-tag"></<span class="hljs-name">div</span>></span>
{{end}}

先ほど作成した form タグの下に Controller から送られてくる postsrange を使って表示させてきます。
名前フィールドは DB に定義していなかったので強制的に名無しさんです。

こんな感じで書いた内容が反映されるようになりました!

まとめ

何度も言ってしまっていますがDB接続設定などあれこれ自動でやってくれると思いきや、やってくれなかったりするところがありますが、公式に設定方法はあたりまえですが書いてあるので冷静に対処すれば良いという印象でした。

とはいえ自分が go になれてないせいがほとんどだとは思いつつも scaffold で生成されたコードが読みにくく、go って雰囲気じゃ書けない言語なんだなと思い知らされています。C言語をちゃんと学んだことのある人はまた違うとは思うのですが。

完全な個人的な比較にはなってしまいますが Ruby on Rails を何も知らずに初めて触ったときはここまで苦労しなかったって印象でいっぱいですw

とっつきとしてはフレームワークを使って成果物を作るのはすごい良いと思いますが、ここまでくると言語自体もちゃんと勉強したいなと思いました。

次回は機能を追加するか、もしくは雰囲気で書いていたGo言語自体を読み解いていくかのどちらかを考えています。