イントロダクション

このチュートリアルでカバーする内容:

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

    前提知識:

  • プログラミング経験

  • 基本的なウェブ技術 (HTTP, HTML) の理解
  • 一部のUNIX/DOSコマンドラインの知識

はじめに

現在、Goを実行するにはFreeBSD、Linux、macOS、またはWindowsマシンが必要です。コマンドプロンプトを表すために$を使用します。

Goをインストールします (詳細はインストール手順を参照)。

このチュートリアル用に新しいディレクトリをGOPATH内に作成し、そこに移動します:

  1. $ mkdir gowiki
  2. $ cd gowiki
  1. ``````bash
  2. package main
  3. import (
  4. "fmt"
  5. "os"
  6. )
  7. `

Go標準ライブラリからfmtおよびosパッケージをインポートします。後で追加の機能を実装する際に、このimport宣言にさらにパッケージを追加します。

データ構造

データ構造を定義することから始めましょう。ウィキは相互に接続されたページのシリーズで構成されており、それぞれにタイトルと本文(ページの内容)があります。ここでは、Pageをタイトルと本文を表す2つのフィールドを持つ構造体として定義します。

  1. type Page struct {
  2. Title string
  3. Body []byte
  4. }

[]byte型は「byteスライス」を意味します。(スライスの詳細についてはスライス: 使用法と内部を参照してください。)Body要素は[]byteであり、stringではありません。なぜなら、これは下で見ることになるioライブラリが期待する型だからです。

  1. ``````bash
  2. func (p *Page) save() error {
  3. filename := p.Title + ".txt"
  4. return os.WriteFile(filename, p.Body, 0600)
  5. }
  6. `

このメソッドのシグネチャは次のようになります: 「これはsaveという名前のメソッドで、pPageへのポインタを受け取ります。パラメータはなく、error型の値を返します。」

このメソッドはPageBodyをテキストファイルに保存します。簡単のために、Titleをファイル名として使用します。

  1. 8進整数リテラル`````0600`````は、`````WriteFile`````への3番目のパラメータとして渡され、ファイルは現在のユーザーのみに読み書き権限を持って作成されるべきことを示します。(詳細についてはUnixマニュアルページ`````open(2)`````を参照してください。)
  2. ページを保存するだけでなく、ページをロードすることも望まれます:
  3. ``````bash
  4. func loadPage(title string) *Page {
  5. filename := title + ".txt"
  6. body, _ := os.ReadFile(filename)
  7. return &Page{Title: title, Body: body}
  8. }
  9. `
  1. 関数は複数の値を返すことができます。標準ライブラリ関数`````os.ReadFile``````````[]byte``````````error`````を返します。`````loadPage`````では、エラーはまだ処理されていません。「ブランク識別子」を表すアンダースコア(`````_`````)シンボルがエラー戻り値を捨てるために使用されています(本質的には、値を何にも割り当てないことになります)。
  2. しかし、`````ReadFile`````がエラーに遭遇した場合はどうなりますか?たとえば、ファイルが存在しないかもしれません。このようなエラーを無視すべきではありません。関数を修正して`````*Page``````````error`````を返すようにしましょう。
  3. ``````bash
  4. func loadPage(title string) (*Page, error) {
  5. filename := title + ".txt"
  6. body, err := os.ReadFile(filename)
  7. if err != nil {
  8. return nil, err
  9. }
  10. return &Page{Title: title, Body: body}, nil
  11. }
  12. `

この関数の呼び出し元は、今や2番目のパラメータをチェックできます。nilであれば、ページが正常にロードされたことになります。そうでなければ、呼び出し元が処理できるerrorになります(詳細については言語仕様を参照してください)。

現時点で、シンプルなデータ構造とファイルへの保存およびファイルからのロード機能があります。私たちが書いたものをテストするためにmain関数を書きましょう:

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

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

プログラムを次のようにコンパイルして実行できます:

  1. $ go build wiki.go
  2. $ ./wiki
  3. This is a sample Page.

(Windowsを使用している場合は、プログラムを実行するために「wiki」と入力する必要があります。「./」なしで。)

これまでに書いたコードを表示するにはここをクリックしてください。

net/httpパッケージの紹介(中間)

シンプルなウェブサーバーの完全な動作例を示します:

  1. //go:build ignore
  2. package main
  3. import (
  4. "fmt"
  5. "log"
  6. "net/http"
  7. )
  8. func handler(w http.ResponseWriter, r *http.Request) {
  9. fmt.Fprintf(w, "Hi there, I love %s!", r.URL.Path[1:])
  10. }
  11. func main() {
  12. http.HandleFunc("/", handler)
  13. log.Fatal(http.ListenAndServe(":8080", nil))
  14. }
  1. 次に、`````http.ListenAndServe`````を呼び出し、ポート8080で任意のインターフェース(`````":8080"`````)でリッスンするように指定します。(今はその2番目のパラメータ`````nil`````について心配しないでください。)この関数は、プログラムが終了するまでブロックします。
  2. `````ListenAndServe`````は常にエラーを返します。予期しないエラーが発生した場合にのみ返されるからです。そのエラーをログに記録するために、関数呼び出しを`````log.Fatal`````でラップします。
  3. `````handler`````関数は`````http.HandlerFunc`````型です。`````http.ResponseWriter``````````http.Request`````を引数として受け取ります。
  4. `````http.ResponseWriter`````値はHTTPサーバーの応答を組み立てます。これに書き込むことで、HTTPクライアントにデータを送信します。
  5. `````http.Request`````は、クライアントのHTTPリクエストを表すデータ構造です。`````r.URL.Path`````はリクエストURLのパスコンポーネントです。末尾の`````[1:]`````は「`````Path`````1文字目から最後までのサブスライスを作成する」を意味します。これにより、パス名の先頭の「/」が削除されます。
  6. このプログラムを実行し、URLにアクセスすると:
  7. ``````bash
  8. http://localhost:8080/monkeys
  9. `

プログラムは次の内容を含むページを表示します:

  1. Hi there, I love monkeys!

net/httpを使用してウィキページを提供する

  1. ``````bash
  2. import (
  3. "fmt"
  4. "os"
  5. "log"
  6. "net/http"
  7. )
  8. `

ユーザーがウィキページを表示できるようにするハンドラーviewHandlerを作成しましょう。これは「/view/」で始まるURLを処理します。

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

再度、_を使用してerrorからの戻り値を無視することに注意してください。これは簡単のために行われており、一般的には悪いプラクティスと見なされます。後でこれに対処します。

最初に、この関数はリクエストURLのパスコンポーネントからページタイトルを抽出します。Path[len("/view/"):]で再スライスされ、リクエストパスの先頭の"/view/"コンポーネントが削除されます。これは、パスが常に"/view/"で始まるため、ページのタイトルの一部ではないからです。

次に、関数はページデータをロードし、シンプルなHTMLの文字列でページをフォーマットし、whttp.ResponseWriterに書き込みます。

このハンドラーを使用するには、main関数を再記述して、httpを初期化し、viewHandlerを使用して/view/パスの下のリクエストを処理します。

  1. func main() {
  2. http.HandleFunc("/view/", viewHandler)
  3. log.Fatal(http.ListenAndServe(":8080", nil))
  4. }

これまでに書いたコードを表示するにはここをクリックしてください。

ページデータ(test.txt)を作成し、コードをコンパイルしてウィキページを提供してみましょう。

test.txtファイルをエディタで開き、文字列「Hello world」(引用符なし)を保存します。

  1. $ go build wiki.go
  2. $ ./wiki

(Windowsを使用している場合は、プログラムを実行するために「wiki」と入力する必要があります。「./」なしで。)

このウェブサーバーが実行されている状態で、http://localhost:8080/view/testにアクセスすると、「test」というタイトルのページが表示され、「Hello world」という言葉が含まれます。

ページの編集

ウィキはページを編集する機能がなければウィキではありません。editHandlerという名前の「編集ページ」フォームを表示するためのハンドラーと、saveHandlerという名前のフォームを介して入力されたデータを保存するためのハンドラーを2つ作成しましょう。

最初に、main()に追加します:

  1. func main() {
  2. http.HandleFunc("/view/", viewHandler)
  3. http.HandleFunc("/edit/", editHandler)
  4. http.HandleFunc("/save/", saveHandler)
  5. log.Fatal(http.ListenAndServe(":8080", nil))
  6. }
  1. ``````bash
  2. func editHandler(w http.ResponseWriter, r *http.Request) {
  3. title := r.URL.Path[len("/edit/"):]
  4. p, err := loadPage(title)
  5. if err != nil {
  6. p = &Page{Title: title}
  7. }
  8. fmt.Fprintf(w, "<h1>Editing %s</h1>"+
  9. "<form action=\"/save/%s\" method=\"POST\">"+
  10. "<textarea name=\"body\">%s</textarea><br>"+
  11. "<input type=\"submit\" value=\"Save\">"+
  12. "</form>",
  13. p.Title, p.Title, p.Body)
  14. }
  15. `

この関数は正常に動作しますが、すべてのハードコーディングされたHTMLは醜いです。もちろん、より良い方法があります。

html/templateパッケージ

  1. 最初に、`````html/template`````をインポートリストに追加する必要があります。`````fmt`````はもはや使用しないので、削除する必要があります。
  2. ``````bash
  3. import (
  4. "html/template"
  5. "os"
  6. "net/http"
  7. )
  8. `

HTMLフォームを含むテンプレートファイルを作成しましょう。edit.htmlという名前の新しいファイルを開き、次の行を追加します:

  1. <h1>Editing {{.Title}}</h1>
  2. <form action="/save/{{.Title}}" method="POST">
  3. <div><textarea name="body" rows="20" cols="80">{{printf "%s" .Body}}</textarea></div>
  4. <div><input type="submit" value="Save"></div>
  5. </form>
  1. ``````bash
  2. func editHandler(w http.ResponseWriter, r *http.Request) {
  3. title := r.URL.Path[len("/edit/"):]
  4. p, err := loadPage(title)
  5. if err != nil {
  6. p = &Page{Title: title}
  7. }
  8. t, _ := template.ParseFiles("edit.html")
  9. t.Execute(w, p)
  10. }
  11. `
  1. `````t.Execute`````メソッドはテンプレートを実行し、生成されたHTML`````http.ResponseWriter`````に書き込みます。`````.Title`````および`````.Body`````のドット識別子は`````p.Title`````および`````p.Body`````を参照します。
  2. テンプレートディレクティブは二重波括弧で囲まれています。`````printf "%s" .Body`````命令は、`````.Body`````をバイトストリームではなく文字列として出力する関数呼び出しです。これは`````fmt.Printf`````への呼び出しと同じです。`````html/template`````パッケージは、テンプレートアクションによって生成されるHTMLが安全で正しく見えることを保証するのに役立ちます。たとえば、ユーザーデータがフォームHTMLを壊さないように、`````>`````の大なり記号を自動的にエスケープし、`````&gt;`````に置き換えます。
  3. テンプレートで作業しているので、`````viewHandler`````のためのテンプレートを`````view.html`````と呼びましょう:
  4. ``````bash
  5. <h1>{{.Title}}</h1>
  6. <p>[<a href="/edit/{{.Title}}">edit</a>]</p>
  7. <div>{{printf "%s" .Body}}</div>
  8. `
  1. ``````bash
  2. func viewHandler(w http.ResponseWriter, r *http.Request) {
  3. title := r.URL.Path[len("/view/"):]
  4. p, _ := loadPage(title)
  5. t, _ := template.ParseFiles("view.html")
  6. t.Execute(w, p)
  7. }
  8. `

両方のハンドラーでほぼ同じテンプレートコードを使用していることに注意してください。この重複を削除するために、テンプレートコードを独自の関数に移動しましょう:

  1. func renderTemplate(w http.ResponseWriter, tmpl string, p *Page) {
  2. t, _ := template.ParseFiles(tmpl + ".html")
  3. t.Execute(w, p)
  4. }

そして、ハンドラーをその関数を使用するように修正します:

  1. func viewHandler(w http.ResponseWriter, r *http.Request) {
  2. title := r.URL.Path[len("/view/"):]
  3. p, _ := loadPage(title)
  4. renderTemplate(w, "view", p)
  5. }
  1. func editHandler(w http.ResponseWriter, r *http.Request) {
  2. title := r.URL.Path[len("/edit/"):]
  3. p, err := loadPage(title)
  4. if err != nil {
  5. p = &Page{Title: title}
  6. }
  7. renderTemplate(w, "edit", p)
  8. }

未実装の保存ハンドラーの登録をmainからコメントアウトすれば、再びプログラムをビルドしてテストできます。これまでに書いたコードを表示するにはここをクリックしてください。

存在しないページの処理

もし/view/APageThatDoesntExistを訪れたらどうなりますか?HTMLを含むページが表示されます。これは、loadPageからのエラー戻り値を無視し、データなしでテンプレートを埋めようとし続けるからです。代わりに、要求されたページが存在しない場合は、クライアントを編集ページにリダイレクトしてコンテンツを作成できるようにするべきです:

  1. func viewHandler(w http.ResponseWriter, r *http.Request) {
  2. title := r.URL.Path[len("/view/"):]
  3. p, err := loadPage(title)
  4. if err != nil {
  5. http.Redirect(w, r, "/edit/"+title, http.StatusFound)
  6. return
  7. }
  8. renderTemplate(w, "view", p)
  9. }
  1. <a name="tmp_8"></a>
  2. ## ページの保存
  3. `````saveHandler`````関数は、編集ページにあるフォームの送信を処理します。`````main`````の関連行のコメントを解除した後、ハンドラーを実装しましょう:
  4. ``````bash
  5. func saveHandler(w http.ResponseWriter, r *http.Request) {
  6. title := r.URL.Path[len("/save/"):]
  7. body := r.FormValue("body")
  8. p := &Page{Title: title, Body: []byte(body)}
  9. p.save()
  10. http.Redirect(w, r, "/view/"+title, http.StatusFound)
  11. }
  12. `

ページタイトル(URLで提供される)とフォームの唯一のフィールドBodyは、新しいPageに保存されます。save()メソッドが呼び出され、データがファイルに書き込まれ、クライアントは/view/ページにリダイレクトされます。

  1. <a name="tmp_9"></a>
  2. ## エラー処理
  3. プログラム内のいくつかの場所でエラーが無視されています。これは悪いプラクティスであり、エラーが発生した場合、プログラムは意図しない動作をすることになります。より良い解決策は、エラーを処理し、ユーザーにエラーメッセージを返すことです。そうすれば、何か問題が発生した場合、サーバーは私たちが望む通りに機能し、ユーザーに通知されます。
  4. 最初に、`````renderTemplate`````でエラーを処理しましょう:
  5. ``````bash
  6. func renderTemplate(w http.ResponseWriter, tmpl string, p *Page) {
  7. t, err := template.ParseFiles(tmpl + ".html")
  8. if err != nil {
  9. http.Error(w, err.Error(), http.StatusInternalServerError)
  10. return
  11. }
  12. err = t.Execute(w, p)
  13. if err != nil {
  14. http.Error(w, err.Error(), http.StatusInternalServerError)
  15. }
  16. }
  17. `
  1. 次に、`````saveHandler`````を修正しましょう:
  2. ``````bash
  3. func saveHandler(w http.ResponseWriter, r *http.Request) {
  4. title := r.URL.Path[len("/save/"):]
  5. body := r.FormValue("body")
  6. p := &Page{Title: title, Body: []byte(body)}
  7. err := p.save()
  8. if err != nil {
  9. http.Error(w, err.Error(), http.StatusInternalServerError)
  10. return
  11. }
  12. http.Redirect(w, r, "/view/"+title, http.StatusFound)
  13. }
  14. `
  1. <a name="tmp_10"></a>
  2. ## テンプレートキャッシング
  3. このコードには非効率性があります: `````renderTemplate`````はページがレンダリングされるたびに`````ParseFiles`````を呼び出します。より良いアプローチは、プログラムの初期化時に`````ParseFiles`````を1回呼び出し、すべてのテンプレートを単一の`````*Template`````に解析することです。次に、[`````ExecuteTemplate`````](https://golang.org/pkg/html/template/#Template.ExecuteTemplate)メソッドを使用して特定のテンプレートをレンダリングできます。
  4. 最初に、`````templates`````という名前のグローバル変数を作成し、`````ParseFiles`````で初期化します。
  5. ``````bash
  6. var templates = template.Must(template.ParseFiles("edit.html", "view.html"))
  7. `
  1. `````ParseFiles`````関数は、テンプレートファイルを識別する任意の数の文字列引数を受け取り、それらのファイルを基本ファイル名に基づいて名前を付けたテンプレートに解析します。プログラムにさらにテンプレートを追加する場合は、それらの名前を`````ParseFiles`````呼び出しの引数に追加します。
  2. 次に、`````renderTemplate`````関数を修正して、適切なテンプレートの名前で`````templates.ExecuteTemplate`````メソッドを呼び出します:
  3. ``````bash
  4. func renderTemplate(w http.ResponseWriter, tmpl string, p *Page) {
  5. err := templates.ExecuteTemplate(w, tmpl+".html", p)
  6. if err != nil {
  7. http.Error(w, err.Error(), http.StatusInternalServerError)
  8. }
  9. }
  10. `

テンプレート名はテンプレートファイル名であるため、".html"tmpl引数に追加する必要があります。

バリデーション

ご覧のとおり、このプログラムには深刻なセキュリティ上の欠陥があります: ユーザーはサーバー上で読み書きされる任意のパスを提供できます。これを軽減するために、タイトルを正規表現で検証する関数を書くことができます。

最初に、"regexp"importリストに追加します。次に、バリデーション式を保存するためのグローバル変数を作成します:

  1. var validPath = regexp.MustCompile("^/(edit|save|view)/([a-zA-Z0-9]+)$")
  1. では、`````validPath`````式を使用してパスを検証し、ページタイトルを抽出する関数を書きましょう:
  2. ``````bash
  3. func getTitle(w http.ResponseWriter, r *http.Request) (string, error) {
  4. m := validPath.FindStringSubmatch(r.URL.Path)
  5. if m == nil {
  6. http.NotFound(w, r)
  7. return "", errors.New("invalid Page Title")
  8. }
  9. return m[2], nil // The title is the second subexpression.
  10. }
  11. `

タイトルが有効な場合、それはnilエラー値とともに返されます。タイトルが無効な場合、関数はHTTP接続に「404 Not Found」エラーを書き込み、ハンドラーにエラーを返します。新しいエラーを作成するには、errorsパッケージをインポートする必要があります。

各ハンドラーにgetTitleの呼び出しを追加しましょう:

  1. func viewHandler(w http.ResponseWriter, r *http.Request) {
  2. title, err := getTitle(w, r)
  3. if err != nil {
  4. return
  5. }
  6. p, err := loadPage(title)
  7. if err != nil {
  8. http.Redirect(w, r, "/edit/"+title, http.StatusFound)
  9. return
  10. }
  11. renderTemplate(w, "view", p)
  12. }
  1. func editHandler(w http.ResponseWriter, r *http.Request) {
  2. title, err := getTitle(w, r)
  3. if err != nil {
  4. return
  5. }
  6. p, err := loadPage(title)
  7. if err != nil {
  8. p = &Page{Title: title}
  9. }
  10. renderTemplate(w, "edit", p)
  11. }
  1. func saveHandler(w http.ResponseWriter, r *http.Request) {
  2. title, err := getTitle(w, r)
  3. if err != nil {
  4. return
  5. }
  6. body := r.FormValue("body")
  7. p := &Page{Title: title, Body: []byte(body)}
  8. err = p.save()
  9. if err != nil {
  10. http.Error(w, err.Error(), http.StatusInternalServerError)
  11. return
  12. }
  13. http.Redirect(w, r, "/view/"+title, http.StatusFound)
  14. }

関数リテラルとクロージャの紹介

各ハンドラーでエラー条件をキャッチすることは、多くの重複したコードを導入します。各ハンドラーをこのバリデーションとエラーチェックを行う関数でラップできればどうでしょうか?Goの関数リテラルは、ここで役立つ機能を抽象化する強力な手段を提供します。

最初に、各ハンドラーの関数定義を再記述して、タイトル文字列を受け取るようにします:

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

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

  1. func makeHandler(fn func (http.ResponseWriter, *http.Request, string)) http.HandlerFunc {
  2. return func(w http.ResponseWriter, r *http.Request) {
  3. // Here we will extract the page title from the Request,
  4. // and call the provided handler 'fn'
  5. }
  6. }

戻り値の関数はクロージャと呼ばれ、外部で定義された値を囲みます。この場合、fn変数(makeHandlerへの単一の引数)がクロージャによって囲まれています。fn変数は、私たちの保存、編集、または表示ハンドラーの1つになります。

これで、getTitleからのコードをここで使用できます(いくつかの小さな修正を加えて):

  1. func makeHandler(fn func(http.ResponseWriter, *http.Request, string)) http.HandlerFunc {
  2. return func(w http.ResponseWriter, r *http.Request) {
  3. m := validPath.FindStringSubmatch(r.URL.Path)
  4. if m == nil {
  5. http.NotFound(w, r)
  6. return
  7. }
  8. fn(w, r, m[2])
  9. }
  10. }
  1. これで、`````makeHandler`````でハンドラー関数を`````main`````でラップし、`````http`````パッケージに登録する前に行います:
  2. ``````bash
  3. func main() {
  4. http.HandleFunc("/view/", makeHandler(viewHandler))
  5. http.HandleFunc("/edit/", makeHandler(editHandler))
  6. http.HandleFunc("/save/", makeHandler(saveHandler))
  7. log.Fatal(http.ListenAndServe(":8080", nil))
  8. }
  9. `

最後に、ハンドラー関数からgetTitleの呼び出しを削除し、はるかにシンプルにします:

  1. func viewHandler(w http.ResponseWriter, r *http.Request, title string) {
  2. p, err := loadPage(title)
  3. if err != nil {
  4. http.Redirect(w, r, "/edit/"+title, http.StatusFound)
  5. return
  6. }
  7. renderTemplate(w, "view", p)
  8. }
  1. func editHandler(w http.ResponseWriter, r *http.Request, title string) {
  2. p, err := loadPage(title)
  3. if err != nil {
  4. p = &Page{Title: title}
  5. }
  6. renderTemplate(w, "edit", p)
  7. }
  1. func saveHandler(w http.ResponseWriter, r *http.Request, title string) {
  2. body := r.FormValue("body")
  3. p := &Page{Title: title, Body: []byte(body)}
  4. err := p.save()
  5. if err != nil {
  6. http.Error(w, err.Error(), http.StatusInternalServerError)
  7. return
  8. }
  9. http.Redirect(w, r, "/view/"+title, http.StatusFound)
  10. }

試してみてください!

最終コードリストを表示するにはここをクリックしてください。

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

  1. $ go build wiki.go
  2. $ ./wiki

http://localhost:8080/view/ANewPageにアクセスすると、ページ編集フォームが表示されるはずです。テキストを入力し、「保存」をクリックすると、新しく作成されたページにリダイレクトされるはずです。

その他のタスク

自分で取り組みたい簡単なタスクをいくつか紹介します:

  • テンプレートをtmpl/に保存し、ページデータをdata/に保存します。
  • ウェブルートを/view/FrontPageにリダイレクトするハンドラーを追加します。
  • ページテンプレートを有効なHTMLにして、いくつかのCSSルールを追加して見栄えを良くします。
  • [PageName]のインスタンスを<a href="/view/PageName">PageName</a>に変換して、ページ間リンクを実装します。(ヒント: regexp.ReplaceAllFuncを使用することができます)