0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

[翻訳]Go言語のWebアプリケーションの書き方(原文: Writing Web Applications)

Posted at

この記事について

この記事は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言語の標準ライブラリからfmtosというパッケージを読み込んでいます。
後々、追加機能を実装していくので、この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はページのデータがどのようにメモリに保存されるかを記述しています。
一方、永続的なストレージについてはどうでしょうか?
Pagesaveメソッドを作成することで解決できます。

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進数の整数0600WriteFileへの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[]byteerrorを返します。LoadPageでは、errorはまだ制御されていません。アンダースコア(_)によって表される「空白識別子」はerrorの返り値をスローするのに使われます。
(要するに何もない所に値を割り当てるということです。)

しかし、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であれば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.ResponseWriterhttp.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.Titlep.Bodyを参照しています。

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

ここではテンプレートを使っているので、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を返します。
MustCompileCompileと異なり、コンパイルに失敗するとパニックを起こしますが、
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`を呼び出します
    }
}

返された関数は、その外側で定義された値を囲むので、クロージャと呼ばれます。
この場合、変数fnmakeHandlerの引数)はクロージャで囲まれています。
変数fnは、saveedit、または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.ResponseWriterhttp.Requestを受け取る関数です (言い換えればhttp.HandlerFuncです)。
クロージャは、リクエストパスからタイトルを抽出し、正規表現validPathでそれを検証します。
タイトルが無効な場合、関数http.NotFoundを使用して、ResponseWriterにエラーが書き込まれます。
タイトルが有効な場合は、ResponseWriterRequesttitleを引数として同封のハンドラ関数fnが呼び出されます。

これで、ハンドラ関数がhttpパッケージに登録される前に、mainmakeHandlerを使ってラップすることができるようになりました。

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を使ってみましょう。)
0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?