このドキュメントは、http://golang.org/doc/codelab/wiki/の翻訳です。


始めに
開始準備
データ構造
httpパッケージの手ほどき(幕間)
wikiページを動かすためにhttpを使用する
ページの編集
templateパッケージ
存在しないページのハンドリング
ページのセーブ
エラーハンドリング
テンプレートのキャッシュ
入力チェック
関数リテラルとクロージャの手ほどき
動かしてみよう!
他にすべきこと

始めに

このcodelabで説明する範囲:

  • ロードおよびセーブメソッドを持つデータ構造の作成
  • httpパッケージを使用してWEBアプリケーションを構築
  • templateパッケージを使用してHTMLテンプレートを処理
  • regexpパッケージを使用してユーザ入力をチェック
  • クロージャの使用

必要な知識:

  • プログラミング経験
  • 基本的なWEBテクノロジー(HTTP、HTML)への理解
  • UNIXコマンドの知識

開始準備

現時点でGoを実行させるためには、Linux、OS X、FreeBSDのいずれかのマシンが必要です。これらの何れも利用できなければ、Linux仮想マシン(VirtualBoxまたは、それに類似するもの)をセットアップするか、仮想専用サーバを使ってみてください。

Go言語をインストールしておきます。(インストール手順を参照ください)

このcodelab用に新しくディレクトリを作成し、そこにcdします。:

$ mkdir ~/gowiki
$ cd ~/gowiki

wiki.goという名前のファイルを作成し、そのファイルをお気に入りのエディタで開いて、下の行を追加してください。

package main

import (
	"fmt"
	"io/ioutil"
	"os"
)

まず、Goの標準ライブラリから、fmtioutilosパッケージをインポートしました。これから機能を加えていく度に、このimport宣言にパッケージを追加していくことになります。

データ構造

データ構造の定義を始めましょう。これから作成しようとしているwikiは、相互に関連した一連のページから成り、各ページは、タイトルとボディ(ページの内容)を持ちます。まずは、タイトルとボディの2フィールドを有する構造体としてPageを定義します。

type Page struct {
	Title	string
	Body	[]byte
}

この[]byte型は、「byteスライス」のことです。(スライスについてもっと知りたいときは、実践Go言語を参照ください。)  Body要素がstringでなく[]byteである理由は、後ほど説明するioライブラリでは、この型が前提となっているためです。

このPage構造体は、ページのデータがメモリ内にどのように格納されるかを定義していますが、データを永続的に格納するにはどうすればよいでしょうか? これは、Pagesaveメソッドを作成することで解決しましょう。

func (p *Page) save() os.Error {
	filename := p.Title + ".txt"
	return ioutil.WriteFile(filename, p.Body, 0600)
}

このメソッドのシグネチャからは、「このメソッドの名前はsaveであり、Pageへのポインタpをレシーバとして取り、パラメータは受け取らず、os.Error型の値を返す」ということが読み取れます。

このメソッドは、PageBodyをテキストファイルとしてセーブします。複雑なることを避け、Titleをファイル名として使用しています。

このsaveメソッドが、os.Error型の値を返しているのは、これがWriteFile(バイトスライスをファイルに書き込むための標準ライブラリの関数)の戻り値の型であるからです。このsaveメソッドは、ファイル書き込み中に発生したエラーをアプリケーション側で何かしらハンドリングできるようにエラー値を返しています。すべてうまく行けば、Page.save()nil(ポインタ、インタフェース、その他一部の型のゼロ値)を返します。

WriteFileの3番目のパラメータとして渡している8進数の定数0600は、このファイルが、カレントユーザのみ読み書き権限を持つように作成されるよう指示しています。(詳細は、Unixのmanページopen(2)を参照ください。)

これと同様に、ページのロードも必要です。:

func loadPage(title string) *Page {
	filename := title + ".txt"
	body, _ := ioutil.ReadFile(filename)
	return &Page{Title: title, Body: body}
}

このloadPage関数は、Titleからファイル名を作成して、そのファイルの内容を新しいPageに読み込み、その新しいPageのポインタを返します。

関数は、複数の値を返せます。標準ライブラリのio.ReadFile関数は、[]byteos.Errorを返します。このloadPageでは、まだエラーをハンドリングしていません。(エラーの戻り値を破棄するために、アンダースコア(_)で表される「ブランク識別子」が使用されています。こうすると、値をどこにも代入しません。)

しかし、例えばファイルが存在しないなど、ReadFileでエラーが起きたときはどうしましょう? やはり、このようなエラーは無視すべきではありません。関数を修正して、*Pageos.Errorを返すようにしてみましょう。

func loadPage(title string) (*Page, os.Error) {
	filename := title + ".txt"
	body, err := ioutil.ReadFile(filename)
	if err != nil {
		return nil, err
	}
	return &Page{Title: title, Body: body}, nil
}

これで、この関数の呼び出し側は、2番目の戻り値をチェックしてnilであれば、ページのロードが成功したことが分かるようになりました。nilでなければ、os.Errorなので、呼び出し側でハンドリングできます。(詳細は、osのパッケージドキュメントを参照ください。)

ここまでで、簡単なデータ構造と、そのデータをファイルにセーブまたはロードする機能が用意できました。続いて、これらをテストするためのmain関数を記述してみましょう。

func main() {
	p1 := &Page{Title: "TestPage", Body: []byte("This is a sample Page.")}
	p1.save()
	p2, _ := loadPage("TestPage")
	fmt.Println(string(p2.Body))
}

このコードをコンパイルし実行してみると、p1の内容が書き込まれたTestPage.txtという名前のファイルが作成されます。続いてこのファイルは、構造体p2に読み込まれ、そのBody要素が画面に出力されます。

このプログラムは、次のようにしてコンパイルと実行ができます。

$ 8g wiki.go
$ 8l wiki.8
$ ./8.out
This is a sample page.

(この8g8lコマンドは、GOARCH=386のときに使います。amd64システム上では、8の代わりに6としてください。)

ここまでに書いたコードを見るには、ここをクリックしてください。

httpパッケージの手ほどき(幕間)

下は、一通り動作する簡単なウェブサーバのサンプルです。:

package main

import (
	"fmt"
	"http"
)

func handler(w http.ResponseWriter, r *http.Request) {
	fmt.Fprintf(w, "Hi there, I love %s!", r.URL.Path[1:])
}

func main() {
	http.HandleFunc("/", handler)
	http.ListenAndServe(":8080", nil)
}

このmain関数では、最初にhttp.HandleFuncを呼び出して、ウェブのルート("/")における全てのリクエストをhandlerでハンドリングするよう、httpパッケージに指示しています。

次に、全インタフェース上のポート8080(":8080")でリッスンするようにhttp.ListenAndServeを呼び出しています。(2番目のパラメータがnilであることを今は気にしないでください。)  この関数は、プログラムが終了するまでブロックし続けます。

このhandler関数は、http.HandlerFunc型です。この関数は、http.ResponseWritehttp.Requestを引数として取ります。

http.ResponseWriterの値によってHTTPサーバのレスポンスが生成されます。これに書き込みを行うことで、HTTPクライアントにデータが送信されます。

http.Requestは、クライアントからのHTTPリクエストを格納したデータ構造です。文字列r.URL.Pathは、リクエストされたURLのパス部分です。後ろの[1:]は、「Path内の1番目の文字から最後までの部分スライスを作成する」ことを意味しています。これは、パス名から先頭の「/」を除去しています。

このプログラムを実行し、下のURLにアクセスしてみてください。:

http://localhost:8080/monkeys

下の内容のページがプログラムによって表示されるはずです。:

Hi there, I love monkeys!

wikiページを動かすためにhttpを使用する

httpパッケージを使用するには、インポートされていなければなりません。:

import (
	"fmt"
	"http"
	"io/ioutil"
	"os"
)

wikiページを表示するためのハンドラを作成しましょう。:

const lenPath = len("/view/")

func viewHandler(w http.ResponseWriter, r *http.Request) {
	title := r.URL.Path[lenPath:]
	p, _ := loadPage(title)
	fmt.Fprintf(w, "<h1>%s</h1><div>%s</div>", p.Title, p.Body)
}

この関数は、始めにリクエストされたURLのパス部分であるr.URL.Pathからページのタイトルを取得します。グローバル定数lenPathは、リクエストパスの先頭部分"/view/"の長さです。Pathは、[lenPath:]によって再びスライスされ、文字列内の先頭6文字が切り捨てられます。これは、パスが常に"/view/"から始まることと、その部分がページのタイトルの一部ではないためです。

この関数はその後、ページデータをロードし、そのページを簡単なHTML文字列としてフォーマットし、http.ResponseWriterであるwに書き込みます。

loadPageからの戻り値os.Errorを無視するために、再び_を使用していることに注意してください。これは、例文を複雑化しないために行っているだけで、一般的には悪い例とされます。これは後ほど修正することにしましょう。

このハンドラを使うために、main関数を作成し、パス/view/配下のリクエストをハンドリングするviewHandlerを使ってhttpの初期化を行います。

func main() {
	http.HandleFunc("/view/", viewHandler)
	http.ListenAndServe(":8080", nil)
}

ここまでに書いたコードを見るには、ここをクリックしてください。

では、ページデータを作成(test.txtとして)して、コードをコンパイルし、wikiページを動かしてみましょう。:

$ echo "Hello world" > test.txt
$ 8g wiki.go
$ 8l wiki.8
$ ./8.out

webサーバが動いている状態で、http://localhost:8080/view/testを訪問すると、タイトルが「test」で、「Hello world」という内容のページが表示されるはずです。

ページの編集

ページが編集できなければwikiとは呼べません。そこで2つのハンドラを新たに作成しましょう。一つ目は、editHandlerという名前で「編集ページ」の入力フォームを表示します。2つ目は、saveHandlerという名前でフォームに入力されたデータをセーブします。

最初に、これらをmain()に加えます。:

func main() {
	http.HandleFunc("/view/", viewHandler)
	http.HandleFunc("/edit/", editHandler)
	http.HandleFunc("/save/", saveHandler)
	http.ListenAndServe(":8080", nil)
}

editHandler関数は、ページをロードし(もしくは存在しなければ、空のPage構造体を作成します)、HTML入力フォームを表示します。

func editHandler(w http.ResponseWriter, r *http.Request) {
	title := r.URL.Path[lenPath:]
	p, err := loadPage(title)
	if err != nil {
		p = &Page{Title: title}
	}
	fmt.Fprintf(w, "<h1>Editing %s</h1>"+
		"<form action=\"/save/%s\" method=\"POST\">"+
		"<textarea name=\"body\">%s</textarea><br>"+
		"<input type=\"submit\" value=\"Save\">"+
		"</form>",
		p.Title, p.Title, p.Body)
}

この関数は正しく動きますが、HTMLがハードエンコードされていて良くありません。もちろん、もっと良い方法があります。

templateパッケージ

templateパッケージは、Goの標準ライブラリの一部です。(新しいtemplateパッケージが出来たので、このcodelabも近々更新します)このtemplateを使って、HTMLを別ファイルに切り出すことで、Goのコードを変更することなく、編集ページのレイアウトを切り替えられるようになります。

まず、インポートのリストにtemplateを加える必要があります。:

import (
	"http"
	"io/ioutil"
	"os"
	"template"
)

HML入力フォームを持つテンプレートファイルを作成しましょう。edit.htmlという名前の新しいファイルを開き、下の行を追加します。:

<h1>Editing {{.Title |html}}</h1>

<form action="/save/{{.Title |html}}" method="POST">
<div><textarea name="body" rows="20" cols="80">{{printf "%s" .Body |html}}</textarea></div>
<div><input type="submit" value="Save"></div>
</form>

editHandlerを変更して、ハードコーディングしたHTMLの代わりにこのテンプレートを使うようにします。:

func editHandler(w http.ResponseWriter, r *http.Request) {
	title := r.URL.Path[lenPath:]
	p, err := loadPage(title)
	if err != nil {
		p = &Page{Title: title}
	}
	t, _ := template.ParseFile("edit.html")
	t.Execute(w, p)
}

template.ParseFile関数は、edit.htmlの内容を読み込み、*template.Templateを返します。

t.Executeメソッドはテンプレートを実行し、生成したHTMLをhttp.ResponseWriterに書き出します。 ドット付きの識別子である.Title.Bodyは、それぞれp.Titlep.Bodyを参照します。

テンプレートのディレクティブは、2重の波括弧でくくられます。「printf "%s" .Body」の指示は、バイトストリームの代わりに文字列として.Bodyの出力を行うための関数呼び出しであり、これはfmt.Printfの呼び出しと同じです。各ディレクティブの「|html」は値をパイプし、出力前にhtmlフォーマッタを通してHTML文字をエスケープ(例えば、>&gt;に置換される)し、ユーザデータがHTMLフォームを壊してしまうのを防げます。

fmt.Fprintfステートメントを取り除いたので、importリストから"fmt"も取り除きます。

テンプレートが使えるようになったので、viewHandlerにもview.htmlという名前のテンプレートを作ります。

<h1>{{.Title |html}}</h1>

<p>[<a href="/edit/{{.Title |html}}">edit</a>]</p>

<div>{{printf "%s" .Body |html}}</div>

次のように、viewHandlerを修正します。:

func viewHandler(w http.ResponseWriter, r *http.Request) {
	title := r.URL.Path[lenPath:]
	p, _ := loadPage(title)
	t, _ := template.ParseFile("view.html")
	t.Execute(w, p)
}

2つのハンドラ内のテンプレート処理のコードが、ほとんど同じことに気付いたでしょうか。この重複部分を、テンプレート処理専用の関数として切り出してみましょう。

func viewHandler(w http.ResponseWriter, r *http.Request) {
	title := r.URL.Path[lenPath:]
	p, _ := loadPage(title)
	renderTemplate(w, "view", p)
}

func editHandler(w http.ResponseWriter, r *http.Request) {
	title := r.URL.Path[lenPath:]
	p, err := loadPage(title)
	if err != nil {
		p = &Page{Title: title}
	}
	renderTemplate(w, "edit", p)
}

func renderTemplate(w http.ResponseWriter, tmpl string, p *Page) {
	t, _ := template.ParseFile(tmpl+".html", nil)
	t.Execute(w, p)
}

これで、ハンドラは簡潔になりました。

存在しないページのハンドリング

/view/APageThatDoesntExistを訪れるとどうなるでしょうか? このプログラムはクラッシュします。こうなる理由は、loadPageからのエラーリターンを無視しているためです。リクエストされたページが存在しなかったときは、クラッシュさせるのではなく、クライアントを編集ページにリダイレクトさせれば、コンテンツを作成してもらえます。:

func viewHandler(w http.ResponseWriter, r *http.Request) {
	title, err := getTitle(w, r)
	if err != nil {
		return
	}
	p, err := loadPage(title)
	if err != nil {
		http.Redirect(w, r, "/edit/"+title, http.StatusFound)
		return
	}
	renderTemplate(w, "view", p)
}

このhttp.Redirect関数では、HTTPステータスコード(http.StatusFound (302))とLocationヘッダをHTTPレスポンスに加えています。

ページのセーブ

saveHandler関数では、サブミットされた入力フォームをハンドリングします。

func saveHandler(w http.ResponseWriter, r *http.Request) {
	title := r.URL.Path[lenPath:]
	body := r.FormValue("body")
	p := &Page{Title: title, Body: []byte(body)}
	p.save()
	http.Redirect(w, r, "/view/"+title, http.StatusFound)
}

ページのタイトル(URLで与えられる)と入力フォーム内の唯一のフィールドであるBodyが、新しいPage内に格納されます。次にsave()メソッドが呼び出されてファイルにデータが書き込まれ、クライアントは、/view/ページにリダイレクトされます。

FormValueから返される値は、string型です。その値をPage構造体に格納する前に[]byteに変換しておかなければなりません。[]byte(body)を使うことで、この変換を行っています。

エラーハンドリング

これまでのプログラム内で、エラーを無視していた箇所が、何ヶ所かありました。これは悪い例であり、特にエラーは、プログラムが暴走する原因となることがあります。最善策は、エラーをハンドリングし、ユーザにエラーメッセージを返すようにします。こうすることで、何かうまくいかなかったときでも、サーバは機能し続け、ユーザには通知が行われます。

始めに、renderTemplate内でエラーをハンドリングしてみましょう。

func renderTemplate(w http.ResponseWriter, tmpl string, p *Page) {
	t, err := template.ParseFile(tmpl+".html", nil)
	if err != nil {
		http.Error(w, err.String(), http.StatusInternalServerError)
		return
	}
	err = t.Execute(w, p)
	if err != nil {
		http.Error(w, err.String(), http.StatusInternalServerError)
	}
}

http.Error関数は、規定されたHTTPレスポンスコード(この場合「Internal Server Error」)とエラーメッセージを送信します。これを個別に各関数内で使用するのは、理にかなっています。

では、saveHandlerを修正しましょう。:

func saveHandler(w http.ResponseWriter, r *http.Request) {
	title, err := getTitle(w, r)
	if err != nil {
		return
	}
	body := r.FormValue("body")
	p := &Page{Title: title, Body: []byte(body)}
	err = p.save()
	if err != nil {
		http.Error(w, err.String(), http.StatusInternalServerError)
		return
	}
	http.Redirect(w, r, "/view/"+title, http.StatusFound)
}

p.save()で発生したエラーは、ユーザに報告されるようになりました。

テンプレートのキャッシュ

renderTemplateは非効率なコードで、ページを生成する度にParseFileを呼び出しています。より良いアプローチとして、プログラムの初期化時に、各テンプレートで一度だけParseFileを呼び出し、その結果得られた*Templateの値を、後で利用できるようにデータ構造内に格納します。

始めに、templatesという名のグローバルマップを作成します。ここに*Templateの値を、string(テンプレート名)をキーとして格納します。:

var templates = make(map[string]*template.Template)

次に、init関数を作成します。これは、プログラム初期化時にmainより先に呼び出されます。template.Must関数は、コンビニエンスラッパー関数で、非nilのos.Errorを返すようなときにパニックを発生します。そうでなければ、通常通り*Templateを返します。ここでパニックを起こすのが適切であるのは、テンプレートがロードできないときに、するべき唯一賢明なことは、プログラムを終了させることだからです。

func init() {
	for _, tmpl := range []string{"edit", "view"} {
		t := template.Must(template.ParseFile(tmpl + ".html"))
		templates[tmpl] = t
	}
}

このforループには、パースしたいテンプレートの名前を格納した配列定数をイテレートするためにrangeステートメントが使われています。このプログラムにこれ以外のテンプレートを追加するときは、この配列にそのテンプレートの名前を加えます。

次に、renderTemplate関数を修正して、templates内の適切なTemplateに対してExecuteメソッドを呼び出すようにします。

func renderTemplate(w http.ResponseWriter, tmpl string, p *Page) {
	err := templates[tmpl].Execute(w, p)
	if err != nil {
		http.Error(w, err.String(), http.StatusInternalServerError)
	}
}

入力チェック

気づいたかも知れませんが、このプログラムには深刻なセキュリティ上の欠陥があり、サーバ上で読み/書きされる任意のパスをユーザーが指定できます。これを取り除くために、正規表現を利用してタイトルの入力チェックを行う関数を用意します。

始めに、importリストに"regexp"を加えます。次に、入力チェック用の正規表現を格納するためのグローバル変数を作成します。:

var titleValidator = regexp.MustCompile("^[a-zA-Z0-9]+$")

regexp.MustCompile関数は、正規表現の解析とコンパイルを行い、regexp.Regexpを返します。このMustCompileは、正規表現式のコンパイルに失敗したとき、Compileでは戻り値の2番目のパラメータとしてos.Errorを返しますが、この関数ではパニックを起こす点がCompileとの違いです。

それでは、リクエストされたURLからタイトル文字列を取り出し、それがTitleValidatorの式と一致するか調べる関数を作成しましょう。:

func getTitle(w http.ResponseWriter, r *http.Request) (title string, err os.Error) {
	title = r.URL.Path[lenPath:]
	if !titleValidator.MatchString(title) {
		http.NotFound(w, r)
		err = os.NewError("Invalid Page Title")
	}
	return
}

タイトルが有効であれば、この関数はnilエラー値とともに返ります。タイトルが不正であれば、この関数は「404 Not Found」エラーをHTTP接続に書き込み、ハンドラにエラーを返します。

では、各ハンドラ内でgetTitleを呼び出すようにしましょう。:

func viewHandler(w http.ResponseWriter, r *http.Request) {
	title, err := getTitle(w, r)
	if err != nil {
		return
	}
	p, err := loadPage(title)
	if err != nil {
		http.Redirect(w, r, "/edit/"+title, http.StatusFound)
		return
	}
	renderTemplate(w, "view", p)
}

func editHandler(w http.ResponseWriter, r *http.Request) {
	title, err := getTitle(w, r)
	if err != nil {
		return
	}
	p, err := loadPage(title)
	if err != nil {
		p = &Page{Title: title}
	}
	renderTemplate(w, "edit", p)
}

func saveHandler(w http.ResponseWriter, r *http.Request) {
	title, err := getTitle(w, r)
	if err != nil {
		return
	}
	body := r.FormValue("body")
	p := &Page{Title: title, Body: []byte(body)}
	err = p.save()
	if err != nil {
		http.Error(w, err.String(), http.StatusInternalServerError)
		return
	}
	http.Redirect(w, r, "/view/"+title, http.StatusFound)
}

関数リテラルとクロージャの手ほどき

各ハンドラそれぞれの中でエラー状態を補足することは、同じコードの繰り返しを招きます。これらのバリデーションとエラーチェックを行う関数内で、各ハンドラをラップできたとしたらどうでしょうか? Go言語の関数リテラルは、ここでの助けとなるような、機能を抽象化するパワフルな手段を提供します。

最初に、各ハンドラの関数の定義をタイトル文字列を受け取るように書き換えます。

func viewHandler(w http.ResponseWriter, r *http.Request, title string)
func editHandler(w http.ResponseWriter, r *http.Request, title string)
func saveHandler(w http.ResponseWriter, r *http.Request, title string)

それでは、上の型の関数を受け取り、http.HandlerFunc型の関数(http.HandleFunc関数に受け渡すのに適した関数)を返すラッパー関数を定義しましょう。:

func makeHandler(fn func (http.ResponseWriter, *http.Request, string)) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		// ここで、Requstからページタイトルを取り出し、
		// 提供したハンドラ'fn'を呼び出すようにします。
	}
}

ここで返される関数は、その外側で定義されている値を内部に取り入れて(enclose)いるためクロージャ(closure)と呼ばれます。この場合、変数fn(makeHandlerの唯一の引数)が、クロージャ内に取り入れられています。この変数fnは、セーブ、編集、表示ハンドラのいずれかです。

それでは、getTitleからコードを取り出し、ここに取り入れます。(若干変更しています。):

func makeHandler(fn func(http.ResponseWriter, *http.Request, string)) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		title := r.URL.Path[lenPath:]
		if !titleValidator.MatchString(title) {
			http.NotFound(w, r)
			return
		}
		fn(w, r, title)
	}
}

makeHandlerによって返されるこのクロージャは、http.ResponseWriterhttp.Requestを受け取る関数で、別の言い方をすれば、http.HandlerFuncです。このクロージャは、リクエストパスからtitleを取り出し、それをregexpであるTitleValidatorを使用して入力チェックを行います。titleが無効のときには、エラーをhttp.NotFound関数を使用してResponseWriterに書き込みを行います。titleが有効なときは、取り入れたハンドラ関数fnが、ResponseWriterRequesttitleを引数として呼び出されます。

それでは、mainでハンドラをhttpパッケージに登録する前に、各ハンドラ関数をmakeHandlerでラップします。:

func main() {
	http.HandleFunc("/view/", makeHandler(viewHandler))
	http.HandleFunc("/edit/", makeHandler(editHandler))
	http.HandleFunc("/save/", makeHandler(saveHandler))
	http.ListenAndServe(":8080", nil)
}

最後に、各ハンドラ関数からgetTitleの呼び出しを取り除き、ハンドラ関数をさらに軽量化します。:

func viewHandler(w http.ResponseWriter, r *http.Request, title string) {
	p, err := loadPage(title)
	if err != nil {
		http.Redirect(w, r, "/edit/"+title, http.StatusFound)
		return
	}
	renderTemplate(w, "view", p)
}

func editHandler(w http.ResponseWriter, r *http.Request, title string) {
	p, err := loadPage(title)
	if err != nil {
		p = &Page{Title: title}
	}
	renderTemplate(w, "edit", p)
}

func saveHandler(w http.ResponseWriter, r *http.Request, title string) {
	body := r.FormValue("body")
	p := &Page{Title: title, Body: []byte(body)}
	err := p.save()
	if err != nil {
		http.Error(w, err.String(), http.StatusInternalServerError)
		return
	}
	http.Redirect(w, r, "/view/"+title, http.StatusFound)
}

動かしてみよう!

完成したコードリストを見るには、ここをクリックしてください。

コードを再コンパイルし、アプリケーションを実行します。:

$ 8g wiki.go
$ 8l wiki.8
$ ./8.out

http://localhost:8080/view/ANewPageを訪れると、編集フォームのページが表示されるはずです。そこで何かしらテキストを入力して「Save」をクリックすると、新たに作成されたページへリダイレクトされます。

他にすべきこと

各自で取り組むのに適した簡単なタスクがいくつか残されています。:

  • テンプレートをtmpl/に、ページデータをdata/に格納する。
  • webのルートを/view/FrontPageへリダイレクトするハンドラを追加する。
  • 適切なHTMLとCSSルールを追加してテンプレートの見栄えを良くする。
  • ページ間リンクの実装([PageName]の部分を<a href="/view/PageName">PageName</a>へ変換する)。 (ヒント:これには、regexp.ReplaceAllFuncを使うとよいでしょう)