Webアプリケーションの作成
はじめに
このチュートリアルでカバーする内容
- ロードおよび保存メソッドを使用したデータ構造の作成
net/http
パッケージを使用したWebアプリケーションの構築html/template
パッケージを使用したHTMLテンプレートの処理regexp
パッケージを使用したユーザー入力の検証- クロージャの使用
前提知識
- プログラミング経験
- 基本的なWebテクノロジー(HTTP、HTML)の理解
- UNIX/DOSコマンドラインの基本的な知識
はじめに
現在、Goを実行するには、FreeBSD、Linux、macOS、またはWindowsマシンが必要です。コマンドプロンプトを表すために$
を使用します。
Goをインストールします(インストール手順を参照)。
このチュートリアルのための新しいディレクトリをGOPATH
の中に作成し、そこにcdします。
$ mkdir gowiki $ cd gowiki
wiki.go
という名前のファイルを作成し、お気に入りのエディターで開き、次の行を追加します。
package main import ( "fmt" "os" )
Go標準ライブラリからfmt
およびos
パッケージをインポートします。後で、追加機能を実装する際に、このimport
宣言にパッケージを追加します。
データ構造
データ構造の定義から始めましょう。Wikiは相互接続された一連のページで構成されており、それぞれにタイトルと本文(ページコンテンツ)があります。ここでは、タイトルと本文を表す2つのフィールドを持つ構造体としてPage
を定義します。
type Page struct { Title string Body []byte }
型[]byte
は「byte
スライス」を意味します。(スライスの詳細については、スライス:使用法と内部構造を参照してください。)Body
要素は、io
ライブラリで使用される型であるため、string
ではなく[]byte
です。これについては後で説明します。
Page
構造体は、ページデータがメモリにどのように格納されるかを記述します。しかし、永続的なストレージについてはどうでしょうか?Page
にsave
メソッドを作成することで、それに対処できます。
func (p *Page) save() error { filename := p.Title + ".txt" return os.WriteFile(filename, p.Body, 0600) }
このメソッドのシグネチャは、「これはsave
という名前のメソッドで、レシーバーとしてp
を取り、Page
へのポインターです。パラメーターは受け取らず、error
型の値を返します。」と読み取れます。
このメソッドは、Page
のBody
をテキストファイルに保存します。簡単にするために、Title
をファイル名として使用します。
save
メソッドは、WriteFile
(バイトスライスをファイルに書き込む標準ライブラリ関数)の戻り値の型であるため、error
値を返します。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
は、[]byte
とerror
を返します。loadPage
では、エラーはまだ処理されていません。アンダースコア(_
)記号で表される「空白の識別子」は、エラーの戻り値を破棄するために使用されます(本質的に、値を何にも割り当てません)。
しかし、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
の場合は、ページのロードに成功しました。そうでない場合は、呼び出し側が処理できる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)) }
このコードをコンパイルして実行すると、p1
の内容を含むTestPage.txt
という名前のファイルが作成されます。その後、ファイルは構造体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))
}
main
関数は、http.HandleFunc
の呼び出しから始まり、Webルート("/"
)へのすべてのリクエストをhandler
で処理するようにhttp
パッケージに指示します。
次に、任意のアドレス(":8080"
)でポート8080をリッスンする必要があることを指定して、http.ListenAndServe
を呼び出します。(今のところ、2番目のパラメーターであるnil
については心配しないでください。)この関数は、プログラムが終了するまでブロックされます。
ListenAndServe
は、予期しないエラーが発生した場合にのみ戻るため、常にエラーを返します。そのエラーをログに記録するために、関数呼び出しをlog.Fatal
でラップします。
関数handler
はhttp.HandlerFunc
型です。http.ResponseWriter
とhttp.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!
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
からページタイトルを抽出します。Path
は、リクエストパスの先頭の"/view/"
コンポーネントを削除するために、[len("/view/"):]
で再度スライスされます。これは、パスが必ず"/view/"
で始まるため、ページタイトルの一部ではないためです。
次に、関数はページデータをロードし、シンプルなHTML文字列でページをフォーマットして、http.ResponseWriter
であるw
に書き込みます。
このハンドラーを使用するには、/view/
パス下のすべてのリクエストを処理するためにviewHandler
を使用してhttp
を初期化するようにmain
関数を書き換えます。
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
」と入力する必要があります。)
このWebサーバーが実行されている場合、http://localhost:8080/view/test
にアクセスすると、「Hello world」という単語を含む「test」というタイトルのページが表示されるはずです。
ページの編集
ページを編集する機能がなければ、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.ParseFiles
は、edit.html
の内容を読み取り、*template.Template
を返します。
メソッドt.Execute
はテンプレートを実行し、生成されたHTMLをhttp.ResponseWriter
に書き込みます。ドット付き識別子.Title
と.Body
は、p.Title
とp.Body
を参照します。
テンプレートディレクティブは二重中括弧で囲まれています。printf "%s" .Body
命令は、.Body
をバイトストリームではなく文字列として出力する関数呼び出しであり、fmt.Printf
の呼び出しと同じです。html/template
パッケージは、テンプレートアクションによって安全で正しい外観のHTMLのみが生成されることを保証するのに役立ちます。たとえば、大なり記号(>
)を自動的にエスケープし、>
に置き換えて、ユーザーデータがフォームHTMLを破損させないようにします。
テンプレートを扱うようになったので、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
で実装されていないsaveハンドラーの登録をコメントアウトすると、再びプログラムをビルドしてテストすることができます。ここをクリックして、これまでに作成したコードを表示してください。
存在しないページの処理
/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)とLocation
ヘッダーをHTTP応答に追加します。
ページの保存
関数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
を1回呼び出し、すべてのテンプレートを単一の*Template
に解析することです。その後、ExecuteTemplate
メソッドを使用して、特定のテンプレートをレンダリングできます。
まず、templates
という名前のグローバル変数を作成し、ParseFiles
で初期化します。
var templates = template.Must(template.ParseFiles("edit.html", "view.html"))
関数template.Must
は、nilでないerror
値が渡された場合にパニックになり、それ以外の場合は変更されていない*Template
を返す便利なラッパーです。ここではパニックが適切です。テンプレートをロードできない場合、実行できる唯一の賢明なことはプログラムを終了することです。
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' } }
返される関数は、外部で定義された値を囲むため、クロージャと呼ばれます。この場合、変数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
)です。クロージャは、リクエストパスからtitle
を抽出し、validPath
正規表現で検証します。title
が無効な場合、http.NotFound
関数を使用して、エラーがResponseWriter
に書き込まれます。title
が有効な場合、囲まれたハンドラー関数fn
がResponseWriter
、Request
、および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
http://localhost:8080/view/ANewPage にアクセスすると、ページ編集フォームが表示されます。次に、テキストを入力し、「保存」をクリックして、新しく作成されたページにリダイレクトできるはずです。
その他のタスク
ここでは、自分自身で取り組みたいと思うかもしれない簡単なタスクをいくつか示します。
- テンプレートを
tmpl/
に、ページデータをdata/
に保存します。 - ウェブのルートを
/view/FrontPage
にリダイレクトするハンドラーを追加します。 - ページテンプレートを有効なHTMLにして、いくつかのCSSルールを追加して、見栄えを良くします。
[PageName]
のインスタンスを
<a href="/view/PageName">PageName</a>
に変換して、ページ間のリンクを実装します。(ヒント:これを行うには、regexp.ReplaceAllFunc
を使用できます)