イントロダクション
このチュートリアルでカバーする内容:
- ロードおよび保存メソッドを持つデータ構造の作成
net/http
パッケージを使用してウェブアプリケーションを構築html/template
パッケージを使用してHTMLテンプレートを処理regexp
パッケージを使用してユーザー入力を検証クロージャの使用
前提知識:
プログラミング経験
- 基本的なウェブ技術 (HTTP, HTML) の理解
- 一部のUNIX/DOSコマンドラインの知識
はじめに
現在、Goを実行するにはFreeBSD、Linux、macOS、またはWindowsマシンが必要です。コマンドプロンプトを表すために$
を使用します。
Goをインストールします (詳細はインストール手順を参照)。
このチュートリアル用に新しいディレクトリをGOPATH
内に作成し、そこに移動します:
$ mkdir gowiki
$ cd gowiki
``````bash
package main
import (
"fmt"
"os"
)
`
Go標準ライブラリからfmt
およびos
パッケージをインポートします。後で追加の機能を実装する際に、このimport
宣言にさらにパッケージを追加します。
データ構造
データ構造を定義することから始めましょう。ウィキは相互に接続されたページのシリーズで構成されており、それぞれにタイトルと本文(ページの内容)があります。ここでは、Page
をタイトルと本文を表す2つのフィールドを持つ構造体として定義します。
type Page struct {
Title string
Body []byte
}
[]byte
型は「byte
スライス」を意味します。(スライスの詳細についてはスライス: 使用法と内部を参照してください。)Body
要素は[]byte
であり、string
ではありません。なぜなら、これは下で見ることになるio
ライブラリが期待する型だからです。
``````bash
func (p *Page) save() error {
filename := p.Title + ".txt"
return os.WriteFile(filename, p.Body, 0600)
}
`
このメソッドのシグネチャは次のようになります: 「これはsave
という名前のメソッドで、p
、Page
へのポインタを受け取ります。パラメータはなく、error
型の値を返します。」
このメソッドはPage
のBody
をテキストファイルに保存します。簡単のために、Title
をファイル名として使用します。
8進整数リテラル`````0600`````は、`````WriteFile`````への3番目のパラメータとして渡され、ファイルは現在のユーザーのみに読み書き権限を持って作成されるべきことを示します。(詳細についてはUnixマニュアルページ`````open(2)`````を参照してください。)
ページを保存するだけでなく、ページをロードすることも望まれます:
``````bash
func loadPage(title string) *Page {
filename := title + ".txt"
body, _ := os.ReadFile(filename)
return &Page{Title: title, Body: body}
}
`
関数は複数の値を返すことができます。標準ライブラリ関数`````os.ReadFile`````は`````[]byte`````と`````error`````を返します。`````loadPage`````では、エラーはまだ処理されていません。「ブランク識別子」を表すアンダースコア(`````_`````)シンボルがエラー戻り値を捨てるために使用されています(本質的には、値を何にも割り当てないことになります)。
しかし、`````ReadFile`````がエラーに遭遇した場合はどうなりますか?たとえば、ファイルが存在しないかもしれません。このようなエラーを無視すべきではありません。関数を修正して`````*Page`````と`````error`````を返すようにしましょう。
``````bash
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))
}
次に、`````http.ListenAndServe`````を呼び出し、ポート8080で任意のインターフェース(`````":8080"`````)でリッスンするように指定します。(今はその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にアクセスすると:
``````bash
http://localhost:8080/monkeys
`
プログラムは次の内容を含むページを表示します:
Hi there, I love monkeys!
net/httpを使用してウィキページを提供する
``````bash
import (
"fmt"
"os"
"log"
"net/http"
)
`
ユーザーがウィキページを表示できるようにするハンドラー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)
}
再度、_
を使用してerror
からの戻り値を無視することに注意してください。これは簡単のために行われており、一般的には悪いプラクティスと見なされます。後でこれに対処します。
最初に、この関数はリクエストURLのパスコンポーネントからページタイトルを抽出します。Path
は[len("/view/"):]
で再スライスされ、リクエストパスの先頭の"/view/"
コンポーネントが削除されます。これは、パスが常に"/view/"
で始まるため、ページのタイトルの一部ではないからです。
次に、関数はページデータをロードし、シンプルなHTMLの文字列でページをフォーマットし、w
、http.ResponseWriter
に書き込みます。
このハンドラーを使用するには、main
関数を再記述して、http
を初期化し、viewHandler
を使用して/view/
パスの下のリクエストを処理します。
func main() {
http.HandleFunc("/view/", viewHandler)
log.Fatal(http.ListenAndServe(":8080", nil))
}
これまでに書いたコードを表示するにはここをクリックしてください。
ページデータ(test.txt
)を作成し、コードをコンパイルしてウィキページを提供してみましょう。
test.txt
ファイルをエディタで開き、文字列「Hello world」(引用符なし)を保存します。
$ go build wiki.go
$ ./wiki
(Windowsを使用している場合は、プログラムを実行するために「wiki
」と入力する必要があります。「./
」なしで。)
このウェブサーバーが実行されている状態で、http://localhost:8080/view/test
にアクセスすると、「test」というタイトルのページが表示され、「Hello world」という言葉が含まれます。
ページの編集
ウィキはページを編集する機能がなければウィキではありません。editHandler
という名前の「編集ページ」フォームを表示するためのハンドラーと、saveHandler
という名前のフォームを介して入力されたデータを保存するためのハンドラーを2つ作成しましょう。
最初に、main()
に追加します:
func main() {
http.HandleFunc("/view/", viewHandler)
http.HandleFunc("/edit/", editHandler)
http.HandleFunc("/save/", saveHandler)
log.Fatal(http.ListenAndServe(":8080", nil))
}
``````bash
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`````をインポートリストに追加する必要があります。`````fmt`````はもはや使用しないので、削除する必要があります。
``````bash
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>
``````bash
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)
}
`
`````t.Execute`````メソッドはテンプレートを実行し、生成されたHTMLを`````http.ResponseWriter`````に書き込みます。`````.Title`````および`````.Body`````のドット識別子は`````p.Title`````および`````p.Body`````を参照します。
テンプレートディレクティブは二重波括弧で囲まれています。`````printf "%s" .Body`````命令は、`````.Body`````をバイトストリームではなく文字列として出力する関数呼び出しです。これは`````fmt.Printf`````への呼び出しと同じです。`````html/template`````パッケージは、テンプレートアクションによって生成されるHTMLが安全で正しく見えることを保証するのに役立ちます。たとえば、ユーザーデータがフォームHTMLを壊さないように、`````>`````の大なり記号を自動的にエスケープし、`````>`````に置き換えます。
テンプレートで作業しているので、`````viewHandler`````のためのテンプレートを`````view.html`````と呼びましょう:
``````bash
<h1>{{.Title}}</h1>
<p>[<a href="/edit/{{.Title}}">edit</a>]</p>
<div>{{printf "%s" .Body}}</div>
`
``````bash
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)
}
<a name="tmp_8"></a>
## ページの保存
`````saveHandler`````関数は、編集ページにあるフォームの送信を処理します。`````main`````の関連行のコメントを解除した後、ハンドラーを実装しましょう:
``````bash
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/
ページにリダイレクトされます。
<a name="tmp_9"></a>
## エラー処理
プログラム内のいくつかの場所でエラーが無視されています。これは悪いプラクティスであり、エラーが発生した場合、プログラムは意図しない動作をすることになります。より良い解決策は、エラーを処理し、ユーザーにエラーメッセージを返すことです。そうすれば、何か問題が発生した場合、サーバーは私たちが望む通りに機能し、ユーザーに通知されます。
最初に、`````renderTemplate`````でエラーを処理しましょう:
``````bash
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)
}
}
`
次に、`````saveHandler`````を修正しましょう:
``````bash
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)
}
`
<a name="tmp_10"></a>
## テンプレートキャッシング
このコードには非効率性があります: `````renderTemplate`````はページがレンダリングされるたびに`````ParseFiles`````を呼び出します。より良いアプローチは、プログラムの初期化時に`````ParseFiles`````を1回呼び出し、すべてのテンプレートを単一の`````*Template`````に解析することです。次に、[`````ExecuteTemplate`````](https://golang.org/pkg/html/template/#Template.ExecuteTemplate)メソッドを使用して特定のテンプレートをレンダリングできます。
最初に、`````templates`````という名前のグローバル変数を作成し、`````ParseFiles`````で初期化します。
``````bash
var templates = template.Must(template.ParseFiles("edit.html", "view.html"))
`
`````ParseFiles`````関数は、テンプレートファイルを識別する任意の数の文字列引数を受け取り、それらのファイルを基本ファイル名に基づいて名前を付けたテンプレートに解析します。プログラムにさらにテンプレートを追加する場合は、それらの名前を`````ParseFiles`````呼び出しの引数に追加します。
次に、`````renderTemplate`````関数を修正して、適切なテンプレートの名前で`````templates.ExecuteTemplate`````メソッドを呼び出します:
``````bash
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)
}
}
`
テンプレート名はテンプレートファイル名であるため、".html"
をtmpl
引数に追加する必要があります。
バリデーション
ご覧のとおり、このプログラムには深刻なセキュリティ上の欠陥があります: ユーザーはサーバー上で読み書きされる任意のパスを提供できます。これを軽減するために、タイトルを正規表現で検証する関数を書くことができます。
最初に、"regexp"
をimport
リストに追加します。次に、バリデーション式を保存するためのグローバル変数を作成します:
var validPath = regexp.MustCompile("^/(edit|save|view)/([a-zA-Z0-9]+)$")
では、`````validPath`````式を使用してパスを検証し、ページタイトルを抽出する関数を書きましょう:
``````bash
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
変数は、私たちの保存、編集、または表示ハンドラーの1つになります。
これで、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`````でハンドラー関数を`````main`````でラップし、`````http`````パッケージに登録する前に行います:
``````bash
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
を使用することができます)