今回はWebブラウザから使える電子掲示板(BBS)を作ってみます。もちろん、Goのさまざまなライブラリを使えば、どれだけでも豪華な高機能なものが作れますが、今回は、Goの良さが引き立つように、「100行位内のプログラム」でかつ「標準ライブラリしか使わない」という制限のもと掲示板を作ってみましょう。

  • 今回作成する掲示板 - 100行位内のプログラムという制約をつけた)

    今回作成する掲示板 - 100行位内のプログラムという制約をつけた)

電子掲示板の製作は基本中の基本

電子掲示板(BBS)とは、訪問者がWebブラウザから自由にコメントを書き込みができるシステムのことです。FacebookやTwitterなどSNSの興隆以前には、主要な情報交換の場所としてよく使われていましたが、最近ではSNSにとって代わられてしまった感じがあります。

とは言え、掲示板の制作は、Web開発の教科書に必ず出てくるお題です。と言うのも、フォームの扱いなど、Webサービスを作る上で欠かせない技術が必要となるからです。そのため、掲示板の製作はWebエンジニアの基本中の基本と言うことができます。

それで、もし掲示板を別途ライブラリなど使わず、その言語の標準ライブラリだけで掲示板を作るなら、その言語の本質を見ることができるかもしれません。それでは、実際のプログラムを確認してみましょう。

掲示板のプログラム

こちらにアップロードされているプログラムを「bbs.go」という名前で保存します。コメントや空行を含めても100行位内(99行)のプログラムです。それでも、100行のプログラムがいきなり記事の中にあると読みにくいと思うのでGistにアップしました。

最初に、プログラムを実行する方法を確認しておきましょう。コマンドラインを起動し、以下のコマンドを入力して、プログラムを実行します。

$ go run bbs.go

そして、Webブラウザを起動して、http://localhost:8888 にアクセスしましょう。すると、掲示板が表示されます。画面上部のフォームから書き込むと内容が表示されます。

  • サーバーを起動してブラウザでアクセスすると掲示板に書き込みができる

    サーバーを起動してブラウザでアクセスすると掲示板に書き込みができる

プログラムを確認しよう

慣れないプログラム言語を100行以上読むのは、なかなか大変です。しかし、100行位内であれば、ちょっと頑張れば読み切ることができます。しかも、大まかにプログラムの構造を掴むことができれば途端にプログラムは分かりやすくなります。大まかな流れを意識しながら解説を確認してみましょう。

まず、今回のプログラムでは、掲示板の書き込みをログをJSON形式でファイルに保存します。下記、プログラム(*1)の部分ではデータの保存先を指定します。

const logFile = "logs.json" // データの保存先 --- (*1)

なお、Go言語は静的型付き言語です。型に緩いJSON形式のデータを読み込む場合も、以下の(*2)のように構造体を使ってJSONのデータ型を宣言しておくなら、Go言語でデータを扱いやすくなります。ここでは、ID、Name(名前)、Body(書き込み本文)、CTime(投稿時間)のフィールドを定義した。このLog構造体のスライス(可変長配列)で掲示板の書き込みデータを表現します。

// Log 掲示板に保存するデータを構造体で定義 --- (*2)
type Log struct {
  ID    int    `json:"id"`
  Name  string `json:"name"`
  Body  string `json:"body"`
  CTime int64  `json:"ctime"`
}

続いて、JSONファイルの内容を読み込む関数loadLogsと保存する関数saveLogsの定義を確認してみましょう。プログラムを意味のある順番で解説するため、ソースコードにふった番号が飛びますが、(*15)の部分で最初にファイルの内容を全部読み込みます。そして、(*16)の部分で読み込んだテキストを構造体Logsのスライスとして読み込みます。そのために、json.Unmarshal関数を使います。

// ファイルからログファイルの読み込み --- (*15)
func loadLogs() []Log {
  // ファイルを開く
  text, err := ioutil.ReadFile(logFile)
  if err != nil {
    return make([]Log, 0)
  }
  // JSONをパース --- (*16)
  var logs []Log
  json.Unmarshal([]byte(text), &logs)
  return logs
}

続いて、書き込みログをJSON形式でファイルに保存する処理(*17)を見てみましょう。ここでは、構造体Logsのスライスをjson.Marshalを利用してJSONエンコード処理を行います。そして、WriteFileを使ってファイルへ保存します。

// ログファイルの書き込み --- (*17)
func saveLogs(logs []Log) {
  // JSONにエンコード
  bytes, _ := json.Marshal(logs)
  // ファイルへ書き込む
  ioutil.WriteFile(logFile, bytes, 0644)
}

ちなみに、PHP、PythonやRubyなどのスクリプト言語であれば、JSONの読み書きなどはもっと短いソースコードで読み込みが記述できます。しかし、型システムを持つGo言語を使うなら間違いが起きにくく、堅牢なシステムを作ることができます。

それから、以下の(*3)の部分ではメインプログラムを定義します。ここでは、Webサーバーを起動するだけです。その際、(*4)のように、どのURIにアクセスがあったとき、どのハンドラ(関数)を実行するのかを指定します。(*5)のListenAndServe関数で実際にサーバーを起動します。ここでは、引数に":8888"を指定していますので、ポート8888番でHTTPサーバーを起動します。

// メインプログラム - サーバーを起動する --- (*3)
func main() {
  println("server - http://localhost:8888")
  // URIに対応するハンドラを登録 --- (*4)
  http.HandleFunc("/", showHandler)
  http.HandleFunc("/write", writeHandler)
  // サーバーを起動 --- (*5)
  http.ListenAndServe(":8888", nil)
}

続く、(*6)の関数showHandlerでは、データファイルを読み込んで画面にログを表示します。まず、(*7)の部分で、ファイルから書き込みログを読み込み、各ログをHTMLにはめ込んでいきます。そして、(*8)の部分でHTMLの全体を出力します。

// 書き込みログを画面に表示する --- (*6)
func showHandler(w http.ResponseWriter, r *http.Request) {
  // ログを読み出してHTMLを生成 --- (*7)
  htmlLog := ""
  logs := loadLogs() // データを読み出す
  for _, i := range logs {
    htmlLog += fmt.Sprintf(
      "<p>(%d) <span>%s</span>: %s --- %s</p>",
      i.ID,
      html.EscapeString(i.Name),
      html.EscapeString(i.Body),
      time.Unix(i.CTime, 0).Format("2006/1/2 15:04"))
  }
  // HTML全体を出力 --- (*8)
  htmlBody := "<html><head><style>" +
    "p { border: 1px solid silver; padding: 1em;} " +
    "span { background-color: #eef; } " +
    "</style></head><body><h1>BBS</h1>" +
    getForm() + htmlLog + "</body></html>"
  w.Write([]byte(htmlBody))
}

そして、以下の(*14)のgetForm関数が画面上部にある書き込みフォームを返します。名前、本文をURI「/write」に向けてPOSTメソッドで送信するという内容になっています。

// 書き込みフォームを返す --- (*14)
func getForm() string {
  return "<div><form action='/write' method='POST'>" +
    "名前: <input type='text' name='name'><br>" +
    "本文: <input type='text' name='body' style='width:30em;'>" +
    "<br><input type='submit' value='書込'>" +
    "</form></div><hr>"
}

上記のフォームを見ながら、以下の(*9)以下の部分を確認してみましょう。(*10)の部分で送信されたフォームデータを解析して、(*11)の部分で既存のデータを読み出します。そして、(*12)の部分で既存データに新規書き込みを追記してファイルへ保存します。そして、データの書き込みが終わったら、(*13)でログ表示を行うルートページへリダイレクトします。

// フォームから送信された内容を書き込み --- (*9)
func writeHandler(w http.ResponseWriter, r *http.Request) {
  r.ParseForm() // フォームを解析 --- (*10)
  var log Log
  log.Name = r.Form["name"][0]
  log.Body = r.Form["body"][0]
  if log.Name == "" {
    log.Name = "名無し"
  }
  logs := loadLogs() // 既存のデータを読み出し --- (*11)
  log.ID = len(logs) + 1
  log.CTime = time.Now().Unix()
  logs = append(logs, log)      // 追記 --- (*12)
  saveLogs(logs)                // 保存
  http.Redirect(w, r, "/", 302) // リダイレクト --- (*13)
}

まとめ

以上、今回は電子掲示板を作ってみました。静的型システムを持つコンパイル言語でありながら、これほど短いソースコードで掲示板が作れるというのは、Go言語のすごいところです。Goに豊富な標準ライブラリが備わっておりのも短いコードで済んだ理由です。Go言語を使ったWebアプリ開発の参考にしてみてください。

自由型プログラマー。くじらはんどにて、プログラミングの楽しさを伝える活動をしている。代表作に、日本語プログラミング言語「なでしこ」 、テキスト音楽「サクラ」など。2001年オンラインソフト大賞入賞、2004年度未踏ユース スーパークリエータ、2010年 OSS貢献者章受賞。技術書も多く執筆している。