Webアプリケーションの作成

はじめに

このチュートリアルで扱う内容

前提知識

はじめに

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

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

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

$ mkdir gowiki
$ cd gowiki

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

package main

import (
    "fmt"
    "os"
)

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

データ構造

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

type Page struct {
    Title string
    Body  []byte
}

[]byteは「byteスライス」を意味します。(スライスについてはスライス:使用法と内部構造を参照してください。)Body要素がstringではなく[]byteなのは、後述するように、使用するioライブラリが期待する型であるためです。

Page構造体は、ページデータがメモリにどのように保存されるかを記述しています。しかし、永続的なストレージはどうでしょうか?Pagesaveメソッドを作成することで、それに対処できます。

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

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

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

saveメソッドはerror値を返します。これはWriteFile(バイトスライスをファイルに書き込む標準ライブラリ関数)の戻り値の型であるためです。saveメソッドはエラー値を返し、ファイル書き込み中に問題が発生した場合にアプリケーションがそれを処理できるようにします。すべてがうまくいけば、Page.save()nil(ポインタ、インターフェース、およびその他のいくつかの型のゼロ値)を返します。

WriteFileの3番目のパラメータとして渡される8進数リテラル0600は、ファイルが現在のユーザーのみに読み書き権限で作成されるべきであることを示します。(詳細についてはUnix manページopen(2)を参照してください。)

ページを保存するだけでなく、ページも読み込みたいと思います。

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

関数loadPageは、タイトルパラメータからファイル名を構築し、ファイルの内容を新しい変数bodyに読み込み、適切なタイトルと本文の値で構築されたPageリテラルへのポインタを返します。

関数は複数の値を返すことができます。標準ライブラリ関数os.ReadFile[]byteerrorを返します。loadPageでは、エラーはまだ処理されていません。アンダースコア(_)記号で表される「ブランク識別子」は、エラーの戻り値を破棄するために使用されます(本質的には、値を何にも代入しないということです)。

しかし、ReadFileがエラーに遭遇した場合はどうなるでしょうか?たとえば、ファイルが存在しない可能性があります。そのようなエラーを無視すべきではありません。関数を*Pageerrorを返すように変更しましょう。

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であれば、ページが正常に読み込まれたことになります。そうでなければ、呼び出し元が処理できる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 パッケージの紹介(余談)

以下は、シンプルなウェブサーバーの完全な動作例です。

//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))
}

main関数はhttp.HandleFuncの呼び出しで始まり、httpパッケージにウェブのルート("/")へのすべてのリクエストをhandlerで処理するように指示します。

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

ListenAndServeは、予期せぬエラーが発生した場合にのみ戻るため、常にエラーを返します。そのエラーをログに記録するために、関数呼び出しをlog.Fatalでラップします。

関数handlerhttp.HandlerFunc型です。これは引数としてhttp.ResponseWriterhttp.Requestを取ります。

http.ResponseWriterの値はHTTPサーバーのレスポンスを組み立てます。それに書き込むことで、HTTPクライアントにデータを送信します。

http.RequestはクライアントのHTTPリクエストを表すデータ構造です。r.URL.PathはリクエストURLのパスコンポーネントです。末尾の[1:]は「Pathの1文字目から最後までをサブスライスとして作成する」ことを意味します。これにより、パス名の先頭の"/"が削除されます。

このプログラムを実行してURLにアクセスすると

https://:8080/monkeys

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

Hi there, I love monkeys!

net/http を使用してWikiページを配信する

net/http パッケージを使用するには、インポートする必要があります。

import (
    "fmt"
    "os"
    "log"
    "net/http"
)

ユーザーがWikiページを表示できるハンドラviewHandlerを作成しましょう。これは「/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からのerror戻り値を無視するために_が使用されていることに注意してください。これはここでは簡潔さのために行われていますが、一般的には悪い習慣とされています。これについては後で対処します。

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

次に、関数はページデータを読み込み、単純なHTMLの文字列でページをフォーマットし、それをwhttp.ResponseWriter)に書き込みます。

このハンドラーを使用するには、main関数を書き換え、viewHandlerを使用してパス/view/以下のすべてのリクエストを処理するようにhttpを初期化します。

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

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

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

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

$ go build wiki.go
$ ./wiki

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

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

ページの編集

ページを編集する機能がなければ、WikiはWikiではありません。新しいハンドラを2つ作成しましょう。1つは「ページ編集」フォームを表示する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標準ライブラリの一部です。html/template を使用してHTMLを別のファイルに保持することで、基盤となるGoコードを変更せずに編集ページのレイアウトを変更できます。

まず、html/templateをインポートリストに追加する必要があります。また、もう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.ParseFilesedit.htmlの内容を読み込み、*template.Templateを返します。

メソッドt.Executeはテンプレートを実行し、生成されたHTMLをhttp.ResponseWriterに書き込みます。.Title.Bodyというドット付き識別子は、p.Titlep.Bodyを参照しています。

テンプレートディレクティブは二重中括弧で囲まれます。printf "%s" .Body命令は、.Bodyをバイトストリームではなく文字列として出力する関数呼び出しであり、fmt.Printfの呼び出しと同じです。html/templateパッケージは、テンプレートアクションによって安全で正しい見栄えのHTMLのみが生成されることを保証するのに役立ちます。例えば、ユーザーデータがフォームHTMLを破損しないように、大なり記号(>)を自動的にエスケープし、&gt;に置き換えます。

テンプレートを扱っているので、viewHandler用にview.htmlというテンプレートを作成しましょう。

<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内の未実装の保存ハンドラの登録をコメントアウトすると、再びプログラムをビルドしてテストすることができます。これまでに書いたコードを見るにはここをクリックしてください。

存在しないページの処理

/view/APageThatDoesntExistにアクセスするとどうなるでしょうか?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でないerror値が渡されるとパニックを起こし、それ以外の場合は*Templateをそのまま返します。ここでパニックを起こすのは適切です。テンプレートがロードできない場合、プログラムを終了する以外に sensible なことはありません。

ParseFiles関数は、テンプレートファイルを識別する任意の数の文字列引数を受け取り、それらのファイルをベースファイル名にちなんで名付けられたテンプレートに解析します。プログラムにさらにテンプレートを追加する場合、それらの名前をParseFiles呼び出しの引数に追加します。

次に、renderTemplate関数を修正して、適切なテンプレート名でtemplates.ExecuteTemplateメソッドを呼び出すようにします。

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は、式コンパイルが失敗した場合にパニックを起こす点でCompileとは異なりますが、Compileは2番目のパラメータとしてerrorを返します。

次に、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.HandlerFunc型の関数を返すラッパー関数を定義しましょう(http.HandleFunc関数に渡すのに適しています)。

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

返される関数は、その外側で定義された値を囲むため、クロージャと呼ばれます。この場合、変数fnmakeHandlerへの単一の引数)はクロージャによって囲まれます。変数fnは、私たちの保存、編集、または表示ハンドラのいずれかになります。

これで、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.ResponseWriterhttp.Request(つまり、http.HandlerFunc)を受け取る関数です。クロージャはリクエストパスからtitleを抽出し、validPath正規表現で検証します。titleが無効な場合、http.NotFound関数を使用してResponseWriterにエラーが書き込まれます。titleが有効な場合、囲まれたハンドラ関数fnが、ResponseWriterRequest、およびtitleを引数として呼び出されます。

これで、ハンドラー関数を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

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

その他のタスク

以下は、自分で取り組んでみたいと思ういくつかの簡単なタスクです。