Go言語(プログラミング)入門メモ㉚-簡単Webアプリケーション-

## テキストを編集して表示するアプリケーションを作成する
web上にテキストを表示して編集できる簡単なWebアプリケーションを作成する。

テキスト表示ページ
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3579705/85c86f74-8792-4ffc-baa3-006f25322533.png)

テキスト編集ページ
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3579705/5ab90611-9cfb-410d-9ec6-2f48b0219fc6.png)

①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要素に入れることができる。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3579705/b9c40d76-360b-48b7-ba95-9e4c79c2e769.png)
次に、後ほど作成する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要素を作成する。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3579705/68fe1a33-7e88-47d9-86c6-a13fb7cd766c.png)

まずは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のテンプレートを基にページを表示している。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3579705/4ed61ef8-e39d-445b-8fd1-20393b142747.png)

[Edit]のリンクをクリックすると「http:/localhost:8080/edit/test」に遷移する。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3579705/376a9d3c-87c6-4ec3-89f8-3e84daccc8c6.png)

#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