この記事について
この記事はGoでWebアプリを作るための公式チュートリアル「Writing Web Applications」の翻訳記事になります(2022/07/26最終更新)。
目次
- イントロダクション
- 始めよう
- データ構造
-
net/http
パッケージの紹介 - wikiのページを配信するために
net/http
を使用する - ページの編集
-
html/template
パッケージ - 存在しないページの扱い
- ページの保存
- エラーハンドリング
- テンプレートキャッシング
- バリデーション
- 関数リテラルとクロージャの紹介
- 試してみる
- 他の課題
イントロダクション
このチュートリアルで扱っていること:
- loadとsaveメソッドを使ったデータ構造の作成
- webアプリケーションを構築するためのnet/httpパッケージの使用
- HTMLテンプレートを処理するためのhtml/templateパッケージの使用
- ユーザの入力を検証するためのregexpパッケージの使用
- クロージャの使用
想定する前提知識:
- プログラミングの経験
- 基本的なweb技術(HTTP, HTML)を理解していること
- コマンドラインの知識(UNIX/DOS)
始めよう
現在、Go言語を実行するためにはFreeBSD、Linux、macOS、Windowsのいずれかのマシンが必要です。
コマンドプロンプトを表現するためには$
記号を使用します。
Goのインストール(インストールの手順を参照)
あなたの$GOPATH
の内側に新しいディレクトリを作成して、そのディレクトリに移動してください
$ mkdir gowiki
$ cd gowiki
wiki.go
という名前のファイルを作成して、好きなエディタでそのファイルを開いて、以下のように入力してください。
package main
import (
"fmt"
"os"
)
Go言語の標準ライブラリからfmt
とos
というパッケージを読み込んでいます。
後々、追加機能を実装していくので、このimport
宣言の中にパッケージを追加していきます。
データ構造
データ構造を定義する所から始めましょう。wikiは相互に接続された1連のページで構成されており、それぞれのページにはタイトルとボディ(本文)があります。
ここでタイトルとボディを表す2つの値を持った構造体をPage
として定義します。
type Page struct {
Title string
Body []byte
}
この[]byte
型は「byte
のslice」という意味です(詳しくはSlices: usage and internalsを参照)。
Body
要素はstring
よりむしろ[]byte
の方が望ましいです。なぜなら、後で使用するi/o
ライブラリの型だからです。
この構造体Page
はページのデータがどのようにメモリに保存されるかを記述しています。
一方、永続的なストレージについてはどうでしょうか?
Page
にsave
メソッドを作成することで解決できます。
func (p *Page) save() error {
filename := p.Title + ".txt"
return os.WriteFile(filename, p.Body, 0600)
}
このメソッドのシグネチャは次の通りです。
「これは、レシーバpをPage
へのポインタとしてとるsave
という名前のメソッドです。これは引数を取らず、error
型の値を返します。 」
このメソッドはPage
のボディをテキストファイルへ保存します。単純化するために、Title
をファイル名として使用します。
このsave
メソッドはWriteFile
(byteのsliceをファイルへ書き込む標準ライブラリの関数)の型を返すため、error
の値を返します。
また、save
メソッドは、ファイルへの書き込み中に発生した問題をアプリケーションが制御するために、error
の値を返します。
もし全て上手くいった場合は、Page.save()
はnil
(ポインタやインタフェースなどのための0値)を返します。
8進数の整数0600
はWriteFile
への3つ目の引数として渡される値であり、現在のユーザのみに対する読み書きの権限と共にファイルが作成されることを示しています。
ページの保存に加えて、ページも読み込みもしたくなってくるでしょう
func loadPage(title string) *Page {
filename := title + ".txt"
body, _ := os.ReadFile(filename)
return &Page{Title: title, Body: body}
}
LoadPage
関数は、引数title
からファイル名を構築し、新しい変数body
へファイルの中身を読み込み、適切なタイトルとボディの値で構成されたPage
リテラルへのポインタを返します。
関数は複数の値を返すことができます。標準ライブラリの関数os.ReadFile
は[]byte
とerror
を返します。LoadPage
では、error
はまだ制御されていません。アンダースコア(_)によって表される「空白識別子」はerrorの返り値をスローするのに使われます。
(要するに何もない所に値を割り当てるということです。)
しかし、ReadFile
がエラーに遭遇した場合は何が起こるのでしょうか?例えば、ファイルが存在しないとしましょう。
そのようなエラーは無視するべきではありません。*Page
とerror
を返すように修正しましょう。
func loadPage(title string) (*Page, error) {
filename := title + ".txt"
body, err := os.ReadFile(filename)
if err != nil {
return nil, err
}
return &Page{Title: title, Body: body}, nil
}
今、この関数の呼び出し元は2個目のパラメタを確認することができます。
もし、それがnil
であればPage
は読み込まれ、そうでない場合は呼び出し元によって制御できるerror
になるでしょう。
この時点で、単純なデータ構造とファイルを保存・読み込みする機能があります。
main
関数を書いて、書かれているコードをテストしてみましょう。
func main() {
p1 := &Page{Title: "TestPage", Body: []byte("This is a sample Page.")}
p1.save()
p2, _ := loadPage("TestPage")
fmt.Println(string(p2.Body))
}
このコードをコンパイルして実行した後、TestPage.txt
という名前のファイルが作成され、それはp1
の中身を含んでいるでしょう。
また、そのファイルは構造体p2によみこまれ、そのBody
要素は画面に表示されるでしょう。
以下のようにプログラムをコンパイルして実行することができます。
$ go build wiki.go
$ ./wiki
This is a sample Page.
(もしWindowsを使用している場合は、プログラムを実行するために"./"を抜いて"wiki"と入力しなければなりません)
net/httpパッケージの紹介
以下のコードは完全に動作するシンプルなwebサーバの例です
//go:build ignore
package main
import (
"fmt"
"log"
"net/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)
log.Fatal(http.ListenAndServe(":8080", nil))
}
メイン関数は http.HandleFunc の呼び出しから始まり、
http パッケージに Web ルート ("/") へのすべてのリクエストをhandler
で処理するよう指示しています。
次にhttp.ListenAndServe
を呼び出し、任意のインターフェイス(":8080")のポート8080をリッスンするよう指定します
(2番目のパラメータは気にしないでください)。
(この関数はプログラムが終了するまでブロックされます。
ListenAndServe
はいつもエラーを返します。なぜなら予期しないエラーが起こった時にだけエラーを返すためです。
そのエラーをログへ出力するために、関数呼び出しをLog.Fatal
で包んでいます。
handler
関数はhttp.HandlerFunc
型のものです。また、http.ResponseWriter
とhttp.Request
を引数として受け取ります。
http.ResponseWriter
の値は、HTTPサーバーのレスポンスを組み立てるもので、
これに書き込むことでHTTPクライアントにデータを送信することができます。
http.Requestは、クライアントのHTTPリクエストを表すデータ構造です。
r.URL.Pathは、リクエストURLのパスコンポーネントです。
末尾の[1:]は、"1文字目から末尾までのPathのサブスライスを作成する"ことを意味します。
これにより、パス名から先頭の"/"が取り除かれます。
もしこのプログラムを実行して、このURLにアクセスしたとすれば
http://localhost:8080/monkeys
このプログラムは以下の文言を含むページを表示するでしょう
Hi there, I love monkeys!
wikiのページを配信するためにnet/httpを使用する
net/http
パッケージを使用するためには、これをインポートしなければなりません
import (
"fmt"
"os"
"log"
"net/http"
)
さあハンドラを作りましょう。viewHandler
はユーザがwikiのページを閲覧できるようにするものです。
また、"/view/"というURLの拡張子を操作します。
func viewHandler(w http.ResponseWriter, r *http.Request) {
title := r.URL.Path[len("/view/"):]
p, _ := loadPage(title)
fmt.Fprintf(w, "<h1>%s</h1><div>%s</div>", p.Title, p.Body)
}
ここでも、loadPageからのエラーの返り値を無視するために_を使用していることに注意してください。
これは、ここでは単純化するために行っていますが、一般的にはバッドプラクティスと考えられています。
これについては後で説明します。
まず、この関数は、リクエストURLのパスコンポーネントであるr.URL.Pathからページタイトルを抽出します。
Pathは[len("/view/"):]で再スライスされ、リクエストパスの先頭の"/view/"コンポーネントが削除されます。
これは、パスが必ず "/view/" で始まるためで、これはページのタイトルの一部ではありません。
この関数は、ページのデータを読み込み、シンプルなHTMLの文字列でページをフォーマットして、http.ResponseWriterのwに書き出します。
このハンドラを使用するために、メイン関数を書き換えて、viewHandlerを使用してhttpを初期化し、
パス/view/以下のすべてのリクエストを処理するようにします。
func main() {
http.HandleFunc("/view/", viewHandler)
log.Fatal(http.ListenAndServe(":8080", nil))
}
いくつかのページデータ(test.txt)を作成し、コードをコンパイルして、wikiページを配信してみましょう。
test.txtをエディタで開き、"Hello world "という文字列を(引用符なしで)保存してください。
$ echo "Hello world" >> test.txt
$ go build wiki.go
$ ./wiki
(Windowsをお使いの場合は、プログラムを実行するために "./" を除いた "wiki" と入力する必要があります)。
このウェブサーバーを起動し、http://localhost:8080/view/test にアクセスすると、
"Hello world" という言葉を含む "test" というタイトルのページが表示されるはずです。
ページの編集
ページを編集する機能なしにはwikiはwikiではありません。
さあ2つの新しいハンドラを作りましょう。
1つは'edit page'フォームを表示するeditHandlerと名前のもので、
もう1つはフォームを通して入力されたデータを保存するsaveHandlerという名前のものを作成しましょう。
最初に、以下のようにmain()に追加します。
func main() {
http.HandleFunc("/view/", viewHandler)
http.HandleFunc("/edit/", editHandler)
http.HandleFunc("/save/", saveHandler)
log.Fatal(http.ListenAndServe(":8080", nil))
}
editHandler関数はページを読み込み(もしくは、存在しない場合は、空の構造体Pageを作ります)、HTMLのフォームを表示します。
func editHandler(w http.ResponseWriter, r *http.Request) {
title := r.URL.Path[len("/edit/"):]
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は醜いものです。もちろん、もっと良い方法がありますよ。
html/templateパッケージ
html/template
パッケージはGoの標準ライブラリの1部です。
html/templateを使用すると、HTMLを別のファイルに保存できるため、
基礎となるGoコードを変更せずに編集ページのレイアウトを変更することができます。
はじめに、html/template
をimportのリストに入れてください。
またfmt
はもう使用しないので、削除してください。
import (
"html/template"
"os"
"net/http"
)
HTMLフォームを含んだテンプレートファイルを作ってみましょう。edit.html
という名前で新しいファイルを開いて、以下の行を追加してください。
<h1>Editing {{.Title}}</h1>
<form action="/save/{{.Title}}" method="POST">
<div><textarea name="body" rows="20" cols="80">{{printf "%s" .Body}}</textarea></div>
<div><input type="submit" value="Save"></div>
</form>
ハードコーディングされたHTMLの代わりに、editHandler
を編集してテンプレートを使用できるようにしてください。
func editHandler(w http.ResponseWriter, r *http.Request) {
title := r.URL.Path[len("/edit/"):]
p, err := loadPage(title)
if err != nil {
p = &Page{Title: title}
}
t, _ := template.ParseFiles("edit.html")
t.Execute(w, p)
}
template.ParseFiles
関数はedit.html
の中身を読み込んで、*template.Template
を返します。
メソッドt.Execute
はテンプレートを実行して、http.ResponseWriter
へ生成されたHTMLを書き込みます。
.Title
と.Body
のドット付き識別子は、p.Title
とp.Body
を参照しています。
テンプレートディレクティブは二重中括弧で囲まれています。
printf "%s" .Body
の文は、.Body
をバイトのストリームではなく文字列として出力する関数呼び出しで、
fmt.Printf.Template
の呼び出しと同じです。
html/template
パッケージは、テンプレートアクションによって安全で正しい外観のHTMLのみが生成されることを保証するのに役立ちます。
例えば、ユーザーデータがフォームのHTMLを破壊しないように、大なり記号(>)を自動的にエスケープして、>
に置き換えます。
ここではテンプレートを使っているので、view.html
というviewHandler
用のテンプレートを作成しましょう。
<h1>{{.Title}}</h1>
<p>[<a href="/edit/{{.Title}}">edit</a>]</p>
<div>{{printf "%s" .Body}}</div>
viewHandler
も同時に修正しましょう
func viewHandler(w http.ResponseWriter, r *http.Request) {
title := r.URL.Path[len("/view/"):]
p, _ := loadPage(title)
t, _ := template.ParseFiles("view.html")
t.Execute(w, p)
}
両方のハンドラでほとんど同じテンプレート・コードを使用していることに注意してください。
テンプレート化するコードを独自の関数に移動させることで、この重複を解消してみましょう。
func renderTemplate(w http.ResponseWriter, tmpl string, p *Page) {
t, _ := template.ParseFiles(tmpl + ".html")
t.Execute(w, p)
}
そしてこの関数を使うためにハンドラを修正します
func viewHandler(w http.ResponseWriter, r *http.Request) {
title := r.URL.Path[len("/view/"):]
p, _ := loadPage(title)
renderTemplate(w, "view", p)
}
func editHandler(w http.ResponseWriter, r *http.Request) {
title := r.URL.Path[len("/edit/"):]
p, err := loadPage(title)
if err != nil {
p = &Page{Title: title}
}
renderTemplate(w, "edit", p)
}
mainの未実装のsaveHandler
をコメントアウトすれば、再びプログラムをビルドしてテストすることができます。
これまでに書いたコードは、ここをクリックしてご覧ください。
存在しないページの扱い
もしあなたが/view/存在しないページのパス
にアクセスしたらどうなるでしょうか?
HTMLを含むページが表示されるでしょう。
これはloadPage
からのエラーの戻り値を無視し、データなしでテンプレートを埋めようとし続けるからです。
代わりに、要求されたページが存在しない場合、コンテンツが作成されるようにクライアントを編集ページにリダイレクトする必要があります。
func viewHandler(w http.ResponseWriter, r *http.Request) {
title := r.URL.Path[len("/view/"):]
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)
のHTTP ステータスコードとLocation ヘッダーを追加します。
ページの保存
関数saveHandler
は、編集ページに配置されたフォームの送信を処理します。
main()
関数の関連行のコメントアウトを削除した後、ハンドラを実装してみましょう。
func saveHandler(w http.ResponseWriter, r *http.Request) {
title := r.URL.Path[len("/save/"):]
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.ParseFiles(tmpl + ".html")
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
err = t.Execute(w, p)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
}
}
http.Error関数は、指定されたHTTPレスポンスコードInternal Server Error
とエラーメッセージを送信します。
すでに、これを別の関数に置くという決断が功を奏しています。
さて、saveHandler
を修正してみましょう。
func saveHandler(w http.ResponseWriter, r *http.Request) {
title := r.URL.Path[len("/save/"):]
body := r.FormValue("body")
p := &Page{Title: title, Body: []byte(body)}
err := p.save()
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
http.Redirect(w, r, "/view/"+title, http.StatusFound)
}
p.save()
の間で起こったエラーはユーザへ知らされます。
テンプレートキャッシング
このコードには効率が良くない点があります。
ページがレンダリングされるたびにrenderTemplate
がParseFiles
を呼び出しています。
よりよい方法は、プログラムの初期化時に一度だけParseFiles
を呼び出し、
すべてのテンプレートを単一の *Template
にパースすることです。
それからExecuteTemplate
メソッドを使って、特定のテンプレートをレンダリングすることができます。
まず、templates
というグローバル変数を作成し、それを ParseFiles で初期化します。
var templates = template.Must(template.ParseFiles("edit.html", "view.html"))
関数template.Must
は、nil
でないエラー値が渡されるとパニックを起こし、
そうでない場合は*Template
をそのまま返す便利なラッパーです。
テンプレートを読み込むことができない場合、プログラムを終了することが唯一の賢明な方法です。
ParseFiles
関数はテンプレートファイルを識別する文字列引数を取り、
それらのファイルをベースのファイル名の後に名付けられたテンプレートにパースします。
プログラムにさらにテンプレートを追加する場合は、その名前をParseFiles
呼び出しの引数に追加します。
次に、適切なテンプレートの名前でtemplates.ExecuteTemplate
メソッドを呼び出すようにrenderTemplate
関数を変更します。
func renderTemplate(w http.ResponseWriter, tmpl string, p *Page) {
err := templates.ExecuteTemplate(w, tmpl+".html", p)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
}
}
なお、テンプレート名はテンプレートファイル名なので、tmpl
の引数に".html "を追加する必要があります。
バリデーション
お気づきのように、このプログラムには重大なセキュリティ上の欠陥があります。
ユーザーがサーバー上で読み書きをするために、任意のパスを供給することができるのです。
これを軽減するために、タイトルを正規表現で検証する関数を書けばよいでしょう。
まず、import
のリストに "regexp "を追加してください。
そして、グローバル変数を作成して、検証式を格納してください。
var validPath = regexp.MustCompile("^/(edit|save|view)/([a-zA-Z0-9]+)$")
関数regexp.MustCompile
は正規表現を解析、コンパイルし、regexp.Regexp.MustCompile
を返します。
MustCompile
はCompile
と異なり、コンパイルに失敗するとパニックを起こしますが、
Compile
は第2パラメータとしてエラーを返します。
では、validPath
式を使って、パスを検証し、ページタイトルを抽出する関数を書いてみましょう。
func getTitle(w http.ResponseWriter, r *http.Request) (string, error) {
m := validPath.FindStringSubmatch(r.URL.Path)
if m == nil {
http.NotFound(w, r)
return "", errors.New("invalid Page Title")
}
return m[2], nil // The title is the second subexpression.
}
タイトルが有効な場合は、nil
のエラー値とともに返されます。
タイトルが無効な場合は、HTTP接続に"404 Not Found"エラーを書き込み、ハンドラにエラーを返します。
新しいエラーを作成するには、errors
パッケージをインポートする必要があります。
各ハンドラーに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.Error(), 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.HandleFunc
に渡すのに適している)
http.HandlerFunc
型の関数を返すラッパー関数を定義してみましょう。
func makeHandler(fn func (http.ResponseWriter, *http.Request, string)) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
// ここでリクエストからページのタイトルを抽出します
// そして与えられているハンドラ`fn`を呼び出します
}
}
返された関数は、その外側で定義された値を囲むので、クロージャと呼ばれます。
この場合、変数fn
(makeHandler
の引数)はクロージャで囲まれています。
変数fn
は、save
、edit
、またはview
ハンドラのいずれかになります。
さて、getTitle
からコードを取り出し少々修正して、ここで使うことができます。
func makeHandler(fn func(http.ResponseWriter, *http.Request, string)) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
m := validPath.FindStringSubmatch(r.URL.Path)
if m == nil {
http.NotFound(w, r)
return
}
fn(w, r, m[2])
}
}
makeHandler
が返すクロージャはhttp.ResponseWriter
とhttp.Request
を受け取る関数です (言い換えればhttp.HandlerFunc
です)。
クロージャは、リクエストパスからタイトルを抽出し、正規表現validPath
でそれを検証します。
タイトルが無効な場合、関数http.NotFound
を使用して、ResponseWriter
にエラーが書き込まれます。
タイトルが有効な場合は、ResponseWriter
、Request
、title
を引数として同封のハンドラ関数fn
が呼び出されます。
これで、ハンドラ関数がhttp
パッケージに登録される前に、main
でmakeHandler
を使ってラップすることができるようになりました。
func main() {
http.HandleFunc("/view/", makeHandler(viewHandler))
http.HandleFunc("/edit/", makeHandler(editHandler))
http.HandleFunc("/save/", makeHandler(saveHandler))
log.Fatal(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.Error(), http.StatusInternalServerError)
return
}
http.Redirect(w, r, "/view/"+title, http.StatusFound)
}
試してみる
コードを再度コンパイルして、コードを実行してください。
$ go build wiki.go
$ ./wiki
http://localhost:8080/view/ANewPageにアクセスすると、ページ編集フォームが表示されるはずです。
テキストを入力し、「保存」をクリックすると、新しく作成されたページに移動することができます。
他の課題
ご自身で取り組まれると良いシンプルな課題を用意しました
- テンプレートを
tmpl/
、ページのデータをdata/
に保存する。 - ルートを
/view/FrontPage
へリダイレクトするハンドラを追加する。 - ページテンプレートを有効なHTMLにし、CSSのルールを追加することで、より洗練されたものにする。
- ページ間のリンクを実装するために、
[PageName]
のインスタンスを<a href="/view/PageName">PageName</a>
へ変更する。(ヒント:regexp.ReplaceAllFunc
を使ってみましょう。)