テキストを編集して表示するアプリケーションを作成する
web上にテキストを表示して編集できる簡単なWebアプリケーションを作成する。
①Webページを管理する構造体を作る
→構造体Page(フィールドにはstring型のTitleとbyte配列のBodyを設定する)
type Page struct {
Title string
Body []byte
}
②構造体Pageにsaveメソッド(ページの情報をテキストファイルに保存する動作のイメージ)
func (p *Page) save() error {
filename := p.Title + ".txt"
return os.WriteFile(filename, p.Body, 0600)
}
メソッドの概要:
saveメソッドは、Page構造体のデータをファイルに保存するためのメソッド。
saveメソッドはPage構造体に関連付けられており、Page構造体のインスタンス(ページオブジェクト)に対して呼び出される。
戻り値はerror型で、エラーが発生した場合にエラーメッセージを返す。
→os.Writefile関数の戻り値は nil or error なのでerror型をsaveメソッドの戻り値をerror型に指定する必要がある。
(nilもerror型の一種)
ポインタレシーバー*Page:
ポインタとは、変数やデータの「アドレス」を指し示すもの。つまり、データそのものではなく、その場所を持つ。
なぜポインタを使うのか:
ポインタを使うことで、データのコピーを避け、メモリ効率を向上させることができ、直接元のデータを操作するため、データの一貫性も保てる。
filename := p.Title + ".txt":
ページのタイトルに.txtを付けて、ファイル名を生成する。
例えば、タイトルが"TestPage"の場合、ファイル名は"TestPage.txt"になる。
return os.WriteFile(filename, p.Body, 0600):
os.WriteFile関数を使用して、生成したファイル名にPageの内容(Bodyフィールド)を書き込みます。
filename:
保存先のファイル名。
p.Body:
ファイルに書き込むデータ(ページの内容)。
0600:
ファイルのパーミッション(アクセス権)を指定している。0600は、ファイルの所有者が読み書きできる権限を表す。
書き込み操作が成功すればnilを、失敗すればエラーメッセージを返す。
③main関数内で、変数p1に構造体Pageを代入して初期化する。また、フィールドのTitleとBodyに値を入れておく。
その後、saveメソッドを実行する。
func main() {
p1 := &Page{Title: "test", Body: []byte("This is a sample Page")}
p1.save()
}
実行すると、「test.txt」という名前のファイルが保存されて「This is a sample Page」と書き込まれている。
④loadPage関数を作成する
テキストファイルからページの情報を読み込む処理を作成する。
loadPage関数の引数をstring型のtitleとし、返り値には*Pageとerrorを返す。
変数filenameに引数titleに「.txt」を加えてファイル名を代入し、os.ReadFile関数で読み込んだ内容を変数bodyに代入する。
エラーハンドリングをして、TitleとBodyに値を代入した構造体Pageのアドレスと、エラーがないのでnilを返り値として返す。
→要約すると、この関数は、指定されたタイトルのテキストファイルを読み込み、その内容をPage構造体として返す関数。エラーが発生した場合は、nilとエラーを返す。
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
}
⑤main関数でloadPage関数で読み込んだ内容を変数p2に代入し、p2.Bodyの内容をstring型にして表示する
func main() {
p1 := &Page{Title: "test", Body: []byte("This is a sample Page")}
p1.save()
p2,_ := loadPage(p1.Title)
fmt.Println(string(p2.Body))
}
⑥URLの登録と、webサーバの立ち上げ
http.HandleFunc関数でURLの登録を行う。
第1引数に「/view/」を指定し、第2引数でこのURLにアクセスが来た時に処理を行うハンドラーの関数(ここでは、viewHandler)を指定する。
webサーバの立ち上げはhttp.ListenAndServe関数で行う。第1引数に「:8080」を指定しwebサーバを立ち上げるポート番号を決め、第2引数ではハンドラー(HTTPリクエストに応答するインターフェース)を指定する。今回は、デフォルトのハンドラーを指定したいのでnilにする。
func main() {
http.HandleFunc("/view/", viewHandler)
log.Fatal(http.ListenAndServe(":8080", nil))
}
次にviewHandler関数を定義する。
関数の引数はそれぞれ、
w http.ResponseWriter: レスポンスを書き込むためのインターフェース。
r *http.Request: リクエストの詳細を持つ構造体へのポインタ。
この引数rに対して「r.URL.Path」のように書くと、URLのパスの情報が取得できる。
例えば。「localhost:8080/view/test」というURLの場合、「view/test」の部分が取得できる。
さらに「test」の部分の文字列を取り出すために「r.URL.Path[len("/view/"):]」と書いて「/view/」の文字列の長さの数よりも後ろの文字列を取得して、変数titleに格納する。
その後、loadPage関数を呼び出して、指定されたタイトルのページを読み込む。
次にfmt.Fprintf関数で、第2引数以降で指定したフォーマットの文字列を、http.ResponseWriterの変数wに書き込み、HTTPレスポンスとしてwebページに表示するHTMLを作る。
ここでは、loadPage関数で酔いこんだページのTitleとBodyの情報を渡して表示させている。
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)
}
このコードを実行するとwebサーバが立ち上がる。
HTMLテンプレートを利用する
上記のコードでは、viewHandler関数の中でfmt.Fprintln関数を使い、直接HTMLを書いていた。もっとHTMLが長くなったとき、別のファイルに切り出して書くことが現実的。
そこで、テンプレートとなるHTMLファイルを作り、text/templateパッケージを使って読み込みを行う。
先ほど作成した「/view/」のURLで表示されるviewページと「/edit/」のURLでviewページの本文の内容を編集できるeditページの2つについて、テンプレートを使用して表示してみる。
viewページの作成。
同じディレクトリに「view.html」を作成する。
次に、vierHandler関数の処理を変更する。fmt.Fprint関数でHTMLを作成するのではなく、renderTemplate関数という独自の関数を作成し、変数wと文字列「view」、変数pを渡してHTTPレスポンスを作成するようにする。
func viewHandler(w http.ResponseWriter, r *http.Request) {
title := r.URL.Path[len("/view/"):]
p, _ := loadPage(title)
renderTemplate(w, "view", p) //renderTemplateを呼び出す処理に変更
}
renderTemplate関数では、template.ParseFiles関数でテンプレートとなるHTMLファイルを作成し、テンプレートを読み込む読み込んだテンプレートを代入した変数tでExcuteメソッドを実行し、変数pに格納されたWebページの情報をhttp.ResponseWriterに書き込む
func renderTemplate(w http.ResponseWriter, tmpl string, p *Page) {
t, _ := template.ParseFiles(tmpl + ".html")
t.Excute(w, p)
}
次に、view.htmlを書く。
<h1>{{.Title}}</h1>
と書くとExcuteメソッドに渡した変数pのTitleをh1要素に入れることができる。
次に、後ほど作成するeditページのリンク(a要素)を作成する。
<p>[<a href="/edit/{{Title}}">Edit</a>]</p>
と書くと、Titleの文字列を含めた「/edit/{{.Title}}」というURLへのリンクを作成できる。
Titleの値が「test」であれば、「/edit/test」というページへのリンクが作れる。
byte配列であるBodyの内容を表示するには、
<div>{{printf "%s" .Body}}</div>
と書いて文字列に変換する。
<h1>{{.Title}}</h1>
<p>[<a href="/edit/{{.Title}}">Edit</a>]</p>
<div>{{printf "%s" .Body}}</div>
editページを作成する
次にeditのページの処理を作成する。main関数で、http.HandleFunc関数の処理を追加し、「/edit/」のURLでeditHandler関数を呼び出す。
func main() {
http.HandleFunc("/view/", viewHandler)
http.HandleFunc("/edit/", editHandler) //「/edit/」のURLを登録
log.Fatal(http.ListenAndServe(":8080", nil))
}
次にedithandler関数を作成する。loadPage関数の処理でエラーハンドリングを行い、ページが読み込めなかった場合は、新しくPage構造体を作成して変数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)
}
edit.htmlを作成する。h1要素は「Edititng{{.Title}}」を書いておく。
続いて、編集しな内容を送信するためのform要素、webページ上で入力可能な枠を作るtextarea要素、そして入力内容を送信するためのボタンとしてinput要素を作成する。
まずはform要素を作る。actionは「/save/{{.Title}}」、methodはprint「Post」とする。これは、form要素の処理を送信するときに、「/save{{.Title}}」のURLに対してPOSTリクエストを送信するという意味。このPOSTリクエストの処理は後で作成する。
textarea要素には、loadPage関数で読み込んだ変数pのBodyの内容を{{printf "%s" .Body}}で表示する。このとき、name属性は「body」とし、
「rws="20"」と「cols="80"」でtextarea要素の大きさを指定する。
最後にinput要素を、「type="submit"」と「value="Save"」を指定して作成する。
<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>
この状態でコードを実行し、「localhost:8080/view/test」にアクセスしてみると、test.txtの内容を読み込んで、view.htmlのテンプレートを基にページを表示している。
[Edit]のリンクをクリックすると「http:/localhost:8080/edit/test」に遷移する。
#3 Webページからの入力内容をファイルに保存する
editページで「Save」のボタンを押したときの処理として、save関数を作る。
main関数でhttp.HandleFunc関数の処理を追加して、「/save/」のURLでsaveHandler関数を呼び出すようにする。
func main() {
http.HandleFunc("/view/", viewHandler)
http.HandleFunc("/edit/", editHandler)
http.HandleFunc("/save/", saveHandler)
log.Fatal(http.ListenAndServe(":8080", nil))
}
続いて、saveHandler関数の処理を作成する。
edit.htmlで入力したform要素の内容は、typeがsubmitのinput要素であるボタンが押されたときに送信され、引数の「r *http.Request」に格納されている。
textarea要素のname属性に「body」と指定しているため、textarea要素の内容はFormValueメソッドを使って取得できる。
取得した情報を変数bodyに格納したらPage構造体のフィールドに値を入れて変数pに代入する。
このPageの情報をsaveメソッドでファイルに保存する。エラーハンドリングを行い、もしエラーが発生した場合はhttp.Error関数を実行する。この時http.StatusInternalServerErrorを指定することで、HTTPステータスコードの500を返す。
エラーがなければ、http.Redirect関数で「"/view/"+title」のURLに遷移させる。HTTPステータスコードは、http.StatusFoundで302を返す。
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)
}
学習に使用した教材
・『入門】Golang基礎入門 + 各種ライブラリ + 簡単なTodoWebアプリケーション開発(Go言語)』M.A EduTech
https://www.udemy.com/course/golang-webgosql/?utm_medium=udemyads&utm_source=bene-msa&utm_campaign=responsive&utm_content=top-1&utm_term=general&msclkid=81e2f24a32cc185d275d953d60760226&couponCode=NEWYEARCAREERJP
・『シリコンバレー一流プログラマーが教える Goプロフェッショナル大全』酒井 潤 (著)
https://www.amazon.co.jp/%E3%82%B7%E3%83%AA%E3%82%B3%E3%83%B3%E3%83%90%E3%83%AC%E3%83%BC%E4%B8%80%E6%B5%81%E3%83%97%E3%83%AD%E3%82%B0%E3%83%A9%E3%83%9E%E3%83%BC%E3%81%8C%E6%95%99%E3%81%88%E3%82%8B-Go%E3%83%97%E3%83%AD%E3%83%95%E3%82%A7%E3%83%83%E3%82%B7%E3%83%A7%E3%83%8A%E3%83%AB%E5%A4%A7%E5%85%A8-%E9%85%92%E4%BA%95-%E6%BD%A4/dp/4046070897