はじめに
本シリーズは2018年の9月の頭から10月末までの約2ヶ月間のGo言語の勉強記録です。大体のWebアプリが備えているような
- HTTPリクエストハンドラ
- データベースとの連携
- セッションマネージャ
を実装するところまで書こうと思っています。
私はプログラミング歴ばっか長くてWebアプリをひとつも作ったことがない。ゆうて行けるっしょってノリでGo言語選んだらしんどかった。です。はい。
今回の記事で使うサンプルコードはGitHubにアップしてあります。
次回「Webアプリ初心者がGo言語でサーバサイド(2. パスワード認証機能の実装)」
勉強方法
私は他の言語の経験は長いですが、Go言語に関してもWebアプリに関しても素人です。最初は以下を参照して進めました。
- 『プログラミング言語Go』(Alan A.A. Donovan, Brian W. Kernighan 著) 丸善
- カニ飯じゃねぇか!!内容はしっかりしているが翻訳がちょっと読みづらい。
- 『Go言語によるWebアプリケーション開発』(Mat Ryer 著) O'REILLY
- 3章まで頑張ればユーザー認証まではできるようになる。
- 『Build web application with Golang』(astaxie 著)
- このクオリティで無料で読めるのはスゴい。
あとはライブラリのチュートリアル、ドキュメント、ソースコードをひたすら読んでいます。
Web上でGo言語に関する記事はとても少ないです。今のところ、各ライブラリのGoDocがもっとも頼りになる情報源だと思います。関数の一覧表からそのソースコードまで1クリックで飛んでいけます。というかググっても公式ドキュメントか GoDoc か GitHub の issue くらいしかヒットしないので必然的にそういう調べ方になります。正しい情報に真っ先に導かれるという意味においてはとても健全ですが、プログラミング自体初心者です、という人にはハードル高めの言語だと思います。
Goを選んだ理由
そうはいってもGo言語はとても魅力的な言語です。Go言語の特徴は、
- 型がある
- コンパイル型で比較的高速である
- 比較的言語仕様が小さい
- 標準ライブラリがしっかりしている
- 比較的粒度の大きなライブラリやツールキットが存在する(gorillaとか)
- Webフレームワークも存在する(gin、irisやechoなど)
という点です。型があるというのはコンパイル時にある程度エラーをはじいてくれるということであり、一般に「動いてから止まられては困る」という特性があるWebアプリには適しています。
また、軽量スレッドである goroutine はメッセージパッシングによって互いに通信することができ、並行処理を記述しやすいなど、サーバサイドに適した特徴を数多く備えています。
以下のような思想を持つ人には好みかもしれません。
- 構文が簡潔で言語仕様が小さいほうがいい(Javaつらい)
- 学習曲線は穏やかであってほしい(Rust怖い, Erlang/OTP怖い)
- 型レベルでわかるエラーはコンパイル時にはじいてほしい(ね。Python, Ruby)
- 暗黙にいろいろしないでほしい(君たちのことだよ JavaScript, PHP)
Webアプリケーションフレームワーク(gin)
実のところ、Go言語はWAF(Web Application Framework)に頼らずともWebアプリが作れてしまうと言われるほど標準ライブラリやツールキットが充実しているのですが、私は初心者かつあまり時間がなかったのでWAFに頼ることにしました。
GoでWebアプリを作ろうと思うと、WAFにはいくつか選択肢がありますが、今回はginを使うことにしました。最初は最速を謳うirisを使っていた(example がとても豊富でけっこう好きだった)のですが、どうも風評があまりよくないらしいのです。ginとirisはインターフェースも似ていますし、速度も大差なさそうなので早めにginに乗り換えました。他にもechoというフレームワークが人気なようです。
ginを使いながらソースコードを少し読んだ感想としては、WAFと言いつつもおおよそnet/http
とgorilla
のラッパーです。httprouter
のおかげで高速だよ、とREADMEに書いてあるので、ルーターの部分にはそのライブラリが使われているのでしょう。Go言語の思想として Ruby on Rails のようなオールインワンの密結合のWAFは作りたがらないようであり、あくまでもいくつかのライブラリを疎に結合して若干使いやすくしたような形になっています。おそらくどのWAFでも同じような構成でしょうから、Go言語に関しては、WAFを選ぶときにそこまで慎重になる必要はないでしょう。
今回作るシステムの構成
今回作るシステムは、下図に示すようなオーソドックスな構成です。
せっかく描いたのでこの図についても詳しく解説しておきます。私たちユーザは普段はClientの側にいて、ブラウザ(Browser)を使ってサーバにアクセスし、サービスを享受するだけです。しかし今回私たちは開発者なのでServerの側にいます。まぁつまり、右側の見るからに複雑で面倒臭いほうの立場になるわけです。
クライアントとサーバはHTTP通信によって情報のやり取りをします。普段私たちがWebページ(この記事も)を閲覧するときは HTTP Request を送り、サーバはそのリクエストに応じてHTMLとCSSを返す、というのが基本的な構図です。
静的なWebページ、すなわち「いつ誰が見ても内容が変わらないようなWebページ」であれば、要求されたページをただ返すだけの単純な機能さえついていれば十分なのですが、現代的なWebアプリケーションはほとんどが動的です。すなわち、いつ誰が見るかによって内容が変わります。
たとえばこのQiitaの記事でも、あなたがいつこの記事を見たかによって👍の数が変わるでしょう(いつ見ても0ということもありえますが、それは私が悲しい)。また、この記事の保持者である私がログインしてアクセスすれば「編集を続ける」というリンクが右上に表示されます。別の誰かがこの記事を見たときに編集ページへのリンクが表示されては大変ですから、「誰が見るかによって内容を変える」という処理をサーバ側で行う必要があります。
HTMLやCSSは静的です。つまりHTMLやCSSには「状況に応じて内容を変える」といった動的な要素を扱う機能はついていません。したがってサーバ側では「リクエストを送ってきたユーザを識別し、適切な情報を記録から読み出して、HTMLやCSSに埋め込んで返す」という処理を行うことになります。この処理を実現するための構成が図の右側になります。
Request Handler はユーザからのリクエストを受け取り、そのリクエストが何をしたがっているものかを解釈して適切な処理を呼び出す部分です。ginでは標準ライブラリのnet/http
を利用しているため、ひとつのリクエストに対してひとつの goroutine が起動します。goroutine はGo言語の軽量スレッドでメインスレッドから切り離されているため、リクエストの処理中にエラーが起こっても通常はサーバ全体が停止するような事態にはなりません。
Session Manager は動的な要素を扱うための機能で、Requestに含まれるCookieなどの情報を用いて、アクセスしてきているユーザを識別し、追跡し続けます。HTMLやCSSは静的なので、もしも Session Manager がなければユーザがページを遷移するごとにログイン情報が失われてしまうことになります。いわゆる「ログインしっぱなし」の状態を維持するための機構がこの Session Manager です。最終的に、Session Manager は KVS(Key-Value Store)である Redis と連携します。
DBMS(DataBase Management System)は、ユーザの情報(ユーザ名やパスワードの他、Qiitaで言えば記事の内容)などを保管しておくためのシステムです。最終的には MySQL を使用します。
HTML,CSS Template は、ユーザに送り返す HTML,CSS の雛形です。簡単に言うと、変化させたい部分をあらかじめ穴埋め形式にしておいたHTMLやCSSです(CSSをテンプレートにすることはまずないかと思いますが)。
Template Engine はセッション、データベースの情報をテンプレートに埋め込む処理を行う部分であり、ginではGo言語の標準ライブラリhtml/template
を用いています。ginが自動でテンプレートエンジンを適用してくれるため、ginでの開発においてはプログラマがこれを意識することは少ないかと思います。
規模の違いによってファイアウォールやロードバランサを挟んだりといった多少の構成の違いこそあれ、ほとんどのWebサービスは以上の構成が基本となっているはずです(多分)。
開発環境
使用したOSはmacOS Mojave バージョン10.14
、golangのバージョンはgo version go1.10.1 darwin/amd64
です。
開発するにあたって仮想環境やプロジェクト管理ツールが使えると便利なのですが、よく使われるツールが不明だったので今回は使用しません。Go言語には一応goenv
やdep
と言ったツールも存在しますが、今のところは.zshrc
に
## Go 環境設定
if [ -x "`which go`" ]; then
export GOPATH=$HOME/Go
export PATH=$PATH:$GOPATH/bin
fi
export PATH="$HOME/.goenv/bin:$PATH"
eval "$(goenv init -)"
を追加して、~/Go/src/
配下に~/Go/src/project
のようにプロジェクトを作成していれば十分かなと思っています。Go言語はパッケージ(Pythonでいうmoduleみたいなもの)をインポートするときにデフォルトで$GOPATH/src
を参照するので、このようにディレクトリ構造を定義しておけば、プロジェクトが大きくなってきて機能をパッケージに小分けしたときに素直にインポートすることができます。たとえば~/Go/src/project/module1
と~/Go/src/project/module2
を
package main
import (
"project/module1"
"project/module2"
)
package module1
import "net/http"
package module2
import "project/module1"
のようにインポートできます。$GOPATH
はユーザが書き換えてもいいのですが、似たような環境変数$GOROOT
はインストール時に設定したら書き換えてはいけないので注意してください。
今のところ使ってみている感想としてGo言語のパッケージは疎結合で依存が少ないです。したがってそもそもPythonのような依存地獄が発生しにくいので、プロジェクトごとに環境を切り替える必要性も薄いのかもしれません。依存でつまずき始めたら考えようと思います(この辺りに関して、ツールなどのいい情報をお持ちの方はお教えいただけると大変嬉しいです)。
簡単なWebサーバ
当面の目標は、パスワードによってユーザを認証し、セッションによってユーザの情報を保持して動的なページを提供できるWebサーバを作ることです。
まずはとりあえずもっとも簡単な、静的なWebサーバを立ててしまいましょう。プロジェクト名はsampleapp
とし、$GOPATH/src/sampleapp
に配置するものとします。
package main
import (
"net/http"
"github.com/gin-gonic/gin"
)
func main() {
router := gin.Default()
router.LoadHTMLGlob("views/*.html")
router.Static("/assets", "./assets")
router.GET("/", func(ctx *gin.Context){
ctx.HTML(http.StatusOK, "index.html", gin.H{})
})
router.Run(":8080")
}
<!DOCTYPE html>
<html lang=ja>
<head>
<meta charset="utf-8">
<meta name="description" content="サンプルアプリケーション">
<title>SampleApp</title>
<link rel="stylesheet" href="//localhost:8080/assets/style.css">
</head>
<body>
<h1>Hello, world!</h1>
</body>
</html>
h1 {
font-size: 32pt;
}
これだけで単純なHTML/CSSは提供できてしまうのですから、スゴいです。
sample$ go run main.go
でサーバを起動できます。サーバはコマンドラインでCtrl+c
で終了することができます。
ブラウザを起動してlocalhost:8080
にアクセスしてみてください。以下のような画面が表示されるはずです。
ソースファイルを分割する/ディレクトリ構造を整理する
さて、これからパスワード認証機能とセッション管理機能を追加していきますが、その前にmain.go
が肥大しないようにルーターとハンドラのソースファイルを分けて、ディレクトリも整理しておきましょう。
ディレクトリ構成は以下のようになります。
sampleapp
├ assets
│ └ [CSSなどを置く]
├ config
│ ├ [データベース周辺]
│ └ dummy_db.go
├ crypto
│ ├ [暗号化に関わる重要なヘルパー]
│ └ crypto.go
├ helpers
│ ├ [validation等のその他のヘルパー]
│ └ helpers.go
├ models
│ └ [モデルを置く]
├ routes
│ ├ [ルーターから呼び出されるハンドラ]
│ ├ routes.go
│ └ user_routes.go
├ sessions
│ ├ [セッション管理]
│ └ dummy_sessions.go
├ views
│ └ [HTMLテンプレートを置く]
└ main.go
コードの全体はGitHubにアップしてあります。
package main
import (
"sampleapp/routes"
"github.com/gin-gonic/gin"
)
func main() {
router := gin.Default()
router.LoadHTMLGlob("views/*.html")
router.Static("/assets", "./assets")
user := router.Group("/user")
{
user.POST("/signup", routes.UserSignUp)
user.POST("/login", routes.UserLogIn)
}
router.GET("/", routes.Home)
router.GET("/login", routes.LogIn)
router.GET("/signup", routes.SignUp)
router.NoRoute(routes.NoRoute)
router.Run(":8080")
}
package routes
import (
"net/http"
"github.com/gin-gonic/gin"
)
func Home(ctx *gin.Context) {
ctx.HTML(http.StatusOK, "index.html", gin.H{})
}
func LogIn(ctx *gin.Context) {
ctx.HTML(http.StatusOK, "login.html", gin.H{})
}
func SignUp(ctx *gin.Context) {
ctx.HTML(http.StatusOK, "signup.html", gin.H{})
}
func NoRoute(ctx *gin.Context) {
ctx.JSON(http.StatusNotFound, gin.H{"code": "PAGE_NOT_FOUND", "message": "Page not found"})
}
package routes
import (
"net/http"
"github.com/gin-gonic/gin"
)
func UserSignUp(ctx *gin.Context) {
println("post/signup")
username := ctx.PostForm("username")
email := ctx.PostForm("emailaddress")
password := ctx.PostForm("password")
passwordConf := ctx.PostForm("passwordconfirmation")
println("username: " + username)
println("email: " + emailaddress)
println("password: " + password)
println("passwordConf: " + passwordConf)
ctx.Redirect(http.StatusSeeOther, "//localhost:8080/")
}
func UserLogIn(ctx *gin.Context) {
println("post/login")
username := ctx.PostForm("username")
password := ctx.PostForm("password")
println("username: " + username)
println("password: " + password)
ctx.Redirect(http.StatusSeeOther, "//localhost:8080/")
}
ログインフォームなどの入力フォームから送信されたリクエストはPOSTリクエストという種類のものになります。POSTリクエストのフィールドを読み取ることで、フォームに入力された内容を受け取ることができます。
また、ログインやサインアップのPOSTは//localhost:8080/user/login
のようにネストしたルートで処理しています。ginではGroup
メソッドによってこれを実現できます。今のところはデータベースもセッションマネージャもないので、とりあえず送られてきた内容をコマンドラインに表示して、直後にホームにリダイレクトしています。
NoRoute
メソッドは、どのルーティングにも当てはまらなかった場合に処理されます。
続いてHTML/CSSですが、新しく追加されるviews/login.html
、views/signup.html
とviews/index.html
には共通部分が生じますので、ついでにHTMLテンプレートの機能を使ってみましょう。
{{ define "head" }}
<!DOCTYPE html>
<html lang=ja>
<head>
<meta charset="utf-8">
<meta name="description" content="サンプルアプリケーション">
<title>SampleApp</title>
<link rel="stylesheet" href="//localhost:8080/assets/style.css">
</head>
{{ end }}
{{ define "foot" }}
</html>
{{ end }}
{{ template "head" }}
<body>
<h1>Welcome to SampleApp.</h1>
<ul>
<li><a href="//localhost:8080/login">login</a>
<li><a href="//localhost:8080/signup">signup</a>
</ul>
</body>
{{ template "foot" }}
{{ template "head" }}
<body>
<article>
This is a test login form.
<form class="login-form" action="//localhost:8080/user/login" method="post">
<label>Username</label><br>
<input type="text" name="username"><br>
<label>Password</label><br>
<input type="password" name="password"><br>
<label>または</label><a href="//localhost:8080/signup">サインアップ</a>
<input type="submit" value="送信">
</form>
<article>
</body>
{{ template "foot" }}
{{ template "head" }}
<body>
<article>
This is a test signup form.
<form class="signup-form" action="//localhost:8080/user/signup" method="post">
<label>Username</label><br>
<input type="text" name="username"><br>
<label>E-mail</label><br>
<input type="text" name="emailaddress"><br>
<label>Password</label><br>
<input type="password" name="password"><br>
<label>Password Confirmation</label><br>
<input type="password" name="passwordconfirmation"><br>
<label>または</label><a href="//localhost:8080/login">ログイン</a>
<input type="submit" value="送信">
</form>
</article>
</body>
{{ template "foot" }}
.login-form {
border: 2px solid #aaa;
width: 300px;
top: 0;
left: 0;
bottom: 0;
right: 0;
margin: auto;
}
.signup-form {
border: 2px solid #aaa;
width: 300px;
top: 0;
left: 0;
bottom: 0;
right: 0;
margin: auto;
}
ガチの初心者なので醜いログインフォームでごめんなさい。フロントエンドはこれから勉強するの。気に入らない人はBootStrapとかをsampleapp/assets
以下に配置して勝手に直してください。というかどうやるのがモダンでスタイリッシュなのか僕に教えてください。
そんでCSSを書いてて「あれ、どうも勝手に変な空白ができたりしてうまく配置できないな」と思っていたのですが、どうやらこれはブラウザのデフォルト設定によるものだそうで、CSSリセットという方法で回避するのがスタンダードっぽいですね。すべてのCSS解説記事の先頭に赤の太字で書いといてよもう!!
CSSリセットを行うには、sample/assets/Reboot.css
に内容をコピーするなどしてください。
ともあれ、ユーザ名とパスワードをサーバ側で受け取ることができました。
次の話へ