7.Webアプリケーションの作成 - iruma-tea/go_programming GitHub Wiki

7. Webアプリケーションの作成

パソコンやスマホのWebブラウザでアクセスしたり操作したりするアプリケーションのことを一般に「Webアプリケーション」という。
WebアプリケーションはHTTPリクエストやJSON、データベースなどといったさまざまな要素で構成されている
ここでは、各要素についてそれぞれ概要を説明しつつ、最後には簡単なWebアプリケーションを作成して理解を深める。

7.1 HTTPリクエストを送信しよう

Webページなどでデータをやりとりするさいには、HTTPという取り決めに従う必要がある。
HTTPにしたがってデータにアクセスする方法には、

  • Webページからデータを取得するGETリクエスト
  • データを登録するPOSTリクエスト Goの標準パッケージを利用して、基本的なHTTPリクエストを送信する方法について説明する。

7.1.1 GETリクエストを送信しよう

  • net/http: WebサーバーやWebクライアントを作るパッケージです。
  • http.Get関数: 指定したURLにGETリクエストを送信できる
    • Getリクエストの結果であるレスポンスボディは、クローズする必要がある。
    • io.ReadAll(resp.Body)で読み取った内容を変数bodyに代入する
    • 最後に「fmt.Println(string(body))」で表示する
import (
    "fmt"
    "io"
    "net/http"
)

func main() {
    resp, _ := http.Get("http://example.com")
    defer resp.Body.Close()
    body, _ := io.ReadAll(resp.Body)
    fmt.Println(string(body))
}
  • net/urlパッケージでURLを解析しよう
    • URLが正しい形式であるかを解析するには、net/urlパッケージをインポートしてurl.Parse関数を使う。
    • 引数に渡した文字列を解析し、戻り値としてURL型の構造体を返す。
    • 文字列がURLの形式でなければエラーを返す。
import (
  "fmt"
  "net/url"
)

func main() {
  base, _ := url.Parse("http://example.com")
  fmt.Println(base)
}
// 実行結果
// http://example.com
func main() {
  base, err := url.Parse("http://e xample.com")
  fmt.Println(base, err)
}
// 実行結果
// <nil> parse "http://e xample.com": invalid character " " in host name
  • クエリパラメータを追加しよう
    • ResolveReferenceメソッドを使用することでクエリーパラメータを追加することができる。
import (
	"fmt"
	"net/url"
)

func main() {
	base, _ := url.Parse("http://example.com")
	reference, _ := url.Parse("/test?a=1&b=2")
	endpoint := base.ResolveReference(reference).String()
	fmt.Println(endpoint)
}
// 実行結果
// http://example.com/test?a=1&b=2

7.1.2 http.NewRequest関数でリクエストを作成しよう

  • http.NewRequest関数でリクエストを作成し、GETリクエストを送信する。
    • リクエストヘッダー
      • HTTPリクエストにはリクエストヘッダーという情報を付加することができる。
      • 次のコードは、「If-None-Match」というヘッダーに適当な値を設定している
import (
  "fmt"
  "net/http"
  "net/url"
)

func main() {
  base , _ := url.Parse("http://example.com")
  reference, _ := url.Parse("/test?a=1&b=2")
  endpoint := base.ResolveReference(reference).String()
  fmt.Println(endpoint)
  req, _ := http.NewRequest("GET", endpoint, nil)
}
// 実行結果
// http://example.com/test?a=1&b=2
// &{GET http://example.com/test?a=1&b=2 HTTP/1.1 1 1 map[] <nil> <nil> 0 [] false example.com map[] map[] <nil> map[]   <nil> <nil> <nil>  {{}} <nil> [] map[]}
  req, _ := http.NewRequest("GET", endpoint, nil)
  req.Header.Add("If-None-Match", `W/"wyzzy"`)
func main() {
  base, _ := url.Parse("http://example.com")
  reference, _ := url.Parse("/test?a=1&b=2")
  endpoint := base.ResolveReference(reference).String()
  fmt.Println(endopoint)
  req, _ := http.NewRequest("GET", endpoint, nil)
  q := req.URL.Query()
  fmt.Println(q)
}
// 実行結果
// map[a:[1] b:[2]]
  • Addメソッドでクエリーパラメータを追加しよう
    • http.NewRequest関数でリクエストを作成する場合は、Addメソッドでクエリーパラメータを追加できる
    • 変数reqのHTTPリクエストに反映するには、「req.URL.RawQuery」に代入する必要がある
      • 代入時には、Encodeメソッドを使ってエンコードする必要がある。
func main() {
  base, _ := url.Parse("http://example.com")
  reference, _ := url.Parse("/test?a=1&b=2")
  endpoint := base.ResolveReference(refernece).String()
  fmt.Println(endpoint)
  req, _ := http.NewRequest("GET", endpoint, nil)
  q := req.URL.Query()
  q.Add("C", "3&%")
  fmt.Println(q)
  fmt.Println(q.Encode())
  req.URL.RawQuery = q.Encode()
}
// 実行結果
// http://example.com/test?a=1&b=2
// map[a:[1] b:[2] c:[3&%]]
// a=1&b=2&c=3%26%25
  • http.ClientでHTTPリクエストを送信しよう
    • 作成したHTTPリクエストを使ってGETメソッドを行うには、http.Clientという型でクライアントを作る必要がある。
    • 「var client *http.Client = &http.Client{}」とクライアントを宣言する。
    • 「client.Do(req)」で変数reqのHTTPリクエストを実行し結果を変数respに代入する。
    • 「io.ReadAll」で内容を表示する
import (
	"fmt"
	"io"
	"net/http"
	"net/url"
)

func main() {
	base, _ := url.Parse("http://example.com")
	reference, _ := url.Parse("/test?a=1&b=2")
	endpoint := base.ResolveReference(reference).String()
	fmt.Println(endpoint)
	req, _ := http.NewRequest("GET", endpoint, nil)
	q := req.URL.Query()
	q.Add("c", "3&%")
	fmt.Println(q)
	fmt.Println(q.Encode())
	req.URL.RawQuery = q.Encode()

	var client *http.Client = &http.Client{}
	resp, _ := client.Do(req)
	body, _ := io.ReadAll(resp.Body)
	fmt.Println(string(body))
}

7.1.3 POSTリクエストを送信しよう

  • POSTリクエストは、http.NewRequestでリクエストを作成する際に、メソッドを"POST"に指定する。
  • POSTの場合は、リクエストボディにデータを入れて渡す。
  • リクエストボディは「bytes.NewBuffer([]byte("password"))」のように3つ目の引数に指定する。
import (
	"bytes"
	"fmt"
	"io"
	"net/http"
	"net/url"
)

func main() {
	base, _ := url.Parse("http://example.com")
	reference, _ := url.Parse("/test?a=1&b=2")
	endpoint := base.ResolveReference(reference).String()
	fmt.Println(endpoint)
	req, _ := http.NewRequest("POST", endpoint, bytes.NewBuffer([]byte("password")))

	var client *http.Client = &http.Client{}
	resp, _ := client.Do(req)
	body, _ := io.ReadAll(resp.Body)
	fmt.Println(string(body))
}

7.2 JSONと構造体を相互に変換しよう

  • JSONとは、「JavaScript Object Notation」の略で、Web上でデータをやりとりする際によく用いられるデータの記述形式。
  • encoding/jsonという標準パッケージを用いることで、JSONのデータをGoの構造体として扱い処理をすることが可能。

7.2.1 json.Unmarshal関数でJSONを構造体に変換しよう

  • eoncoding/jsonはJSONを扱う標準パッケージです。
    • JSONで入ってきたデータをPersonという構造体に格納して表示する
      • JSONのデータはbyteで作成し変数bに代入する。
      • Person型の変数pを宣言して、json.Unmarshal関数に変数pを渡す。
      • エラーハンドリングをして、エラーがなければ変数のpの値をそれぞれ表示する。
    • json.Unmarshal関数はJSONのキーと構造体のフィールド名を比較して値を代入する
      • 構造体Personのフィールドはパブリックなので、頭文字を大文字にしている。
      • json.Unmarshal関数は大文字か小文字かに関わらず名前が一致すればよいので、問題なく構造体Personに値が代入される。
import (
  "encoding/json"
  "fmt"
)

type Person struct {
  Name string
  Age int
  Nicknames []string
}

func main() {
  b := []byte(`{"name":"mike","age":20,"nicknames":["a","b","c"]}`)
  var p Person
  if err := json.Unmarshal(b, &p); err != nil {
    fmt.Println(err)
  }
  fmt.Println(p.Name, p.Age, p.Nicknames)
}
// 実行結果
// mike 20 [a b c]

7.2.2 json.Marshal関数で構造体をJSONに変換しよう

  • 構造体のデータをJSONに変換するには、json.Marshal関数を使う。
import (
	"encoding/json"
	"fmt"
)

type Person struct {
	Name      string
	Age       int
	Nicknames []string
}

func main() {
	b := []byte(`{"name":"mike", "age":20, "nicknames":["a","b","c"]}`)
	var p Person
	if err := json.Unmarshal(b, &p); err != nil {
		fmt.Println(err)
	}
	fmt.Println(p.Name, p.Age, p.Nicknames)

	v, _ := json.Marshal(p)
	fmt.Println(string(v))
}
// 実行結果
// mike 20 [a b c]
// {"Name":"mike","Age":20,"Nicknames":["a","b","c"]}
  • JSONに変換する際のキー名を指定しよう
    • 構造体Personのフィールド名(Name、Age、Nicknames)はJSONに変換した際も頭文字が大文字になっている。
    • 構造体のフィールド名がJSONのキーに変換されるときの名前を指定することができる
      • 構造体Personのフィールド名の後ろで「json:"name"」などのように名前に指定する。
import (
	"encoding/json"
	"fmt"
)

type Person struct {
  Name string `json:name`
  Age int `json:age`
  Nicknames []string `json:nicknames`
}

func main() {
  b := []byte(`{"name":"mike", "age":20, "nicknames":["a","b","c"]}`)
  var p Person
  if err := json.Unmarshal(b, &p) err != nil {
    fmt.Println(err)
  }
 	fmt.Println(p.Name, p.Age, p.Nicknames)
  v, _ := json.Marshal(p)
  fmt.Println(string(v))
}
// 実行結果
// mike 20 [a b c]
// {"name":"mike","age":20,"nicknames":["a","b","c"]}
  • JSONでの型を変更しよう
    • 構造体とJSONとの間で変換を行うときに、違う型として扱うこともできる。
      • 「json:"age,string"」とした場合
        • json.Marshal関数でJSONに変換したときのageの値はstring型になる。
        • json.UnMarshal関数で構造体Personに渡すJSONのageの値はstring型にする必要がある。
import (
	"encoding/json"
	"fmt"
)

type Person struct {
  Name string `json:name`
  Age int `json:age,string`
  Nicknames []string `json:nicknames`
}

func main() {
	b := []byte(`{"name":"mike", "age":"20", "nicknames":["a","b","c"]}`)
	var p Person
	if err := json.Unmarshal(b, &p); err != nil {
		fmt.Println(err)
	}
	fmt.Println(p.Name, p.Age, p.Nicknames)

	v, _ := json.Marshal(p)
	fmt.Println(string(v))
}
// 実行結果
// mike 20 [a b c]
// {"name":"mike","age":"20","nicknames":["a","b","c"]}
  • JSONに変換するときに非表示にしよう
    • 「json:"-"」と指定すると、そのフィールドはjson.Unmarshal、json.Marshal関数の返還時に、JSON側では無視される。
import (
	"encoding/json"
	"fmt"
)

type Person struct {
	Name      string   `json:"-"`
	Age       int      `json:"age,string"`
	Nicknames []string `json:"nicknames"`
}

func main() {
	b := []byte(`{"name":"mike", "age":"20", "nicknames":["a","b","c"]}`)
	var p Person
	if err := json.Unmarshal(b, &p); err != nil {
		fmt.Println(err)
	}
	fmt.Println(p.Name, p.Age, p.Nicknames)

	v, _ := json.Marshal(p)
	fmt.Println(string(v))
}
// 実行結果
//  20 [a b c]
// {"age":"20","nicknames":["a","b","c"]}

7.2.3 omitemptyでデフォルト値を省略しよう

  • int型の数値が0のようなデフォルト値の場合、omitemptyを指定することで、JSONに反映させず省略することが可能。
import (
	"encoding/json"
	"fmt"
)

type Person struct {
	Name      string   `json:"-"`
	Age       int      `json:"age,omitempty"`
	Nicknames []string `json:"nicknames"`
}

func main() {
	b := []byte(`{"name":"mike", "age":0, "nicknames":["a","b","c"]}`)
	var p Person
	if err := json.Unmarshal(b, &p); err != nil {
		fmt.Println(err)
	}
	fmt.Println(p.Name, p.Age, p.Nicknames)

	v, _ := json.Marshal(p)
	fmt.Println(string(v))
}
// 実行結果
//  0 [a b c]
// {"nicknames":["a","b","c"]}
  • 空の構造体にomitemptyを適用する
    • 空の構造体を作り、構造体Personのフィールドに追加する。
    • 次のコードを実行すると、空の構造体が出力される。
      • この場合、Tをポインタ型にして再度、実行すると消える。
import (
	"encoding/json"
	"fmt"
)

type T struct {}

type Person struct {
  Name string `json:"name,omitempty"`
  Age int `json:"age,omitempty"`
  Nicknames []string `json:"nicknames,omitempty"`
  T T `json:"T,omitempty"`
}

func main() {
  b := []byte(`{"name":"","age":20,"nicknames":[]}`)
  var p Person
  if err := json.Unmarshal(b, &p); err != nil {
    fmt.Println(err)
  }
  fmt.Println(p.Name, p.Age, p.Nicknames)

  v, _ := json.Marshal(p)
  fmt.Println(string(v))
}
// 実行結果
//  20 []
// {"age":20,"T":{}}
import (
	"encoding/json"
	"fmt"
)

type T struct {}

type Person struct {
  Name string `json:"name,omitempty"`
  Age int `json:"age,omitempty"`
  Nicknames []string `json:"nicknames,omitempty"`
  T *T `json:"T,omitempty"`
}

func main() {
  b := []byte(`{"name":"","age":20,"nicknames":[]}`)
  var p Person
  if err := json.Unmarshal(b, &p); err != nil {
    fmt.Println(err)
  }
  fmt.Println(p.Name, p.Age, p.Nicknames)

  v, _ := json.Marshal(p)
  fmt.Println(string(v))
}
// 実行結果
//  20 []
// {"age":20}

7.2.4 json.Marshal関数をカスタマイズしよう

  • json.Marshal関数で構造体をJSONに変換する際の処理を、独自にカスタマイズすることも可能
    • 独自にカスタマイズする場合、独自の処理をしたい型にMarshalJSONメソッドを実装する。
import (
	"encoding/json"
	"fmt"
)

type T struct {
}

type Person struct {
	Name      string   `json:"name,omitempty"`
	Age       int      `json:"age,omitempty"`
	Nicknames []string `json:"nicknames,omitempty"`
	T         *T       `json:"T,omitempty"`
}

func (p Person) MarshalJSON() ([]byte, error) {
  v, err := json.Marshal(&struct {
    Name string
  }{
    Name: "Mr." + p.Name,
  })
  return v, err
}

func main() {
  b := []byte(`{"name":"","age":20,"nicknames":[]}`)
  var p Person
  if err := json.Unmarshal(b, &p); err != nil {
    fmt.Println(err)
  }
  fmt.Println(p.Name, p.Age, p.Nicknames)

  v, _ := json.Marshal(p)
  fmt.Println(string(v))
}
// 実行結果
//  20 []
// {"Name":"Mr."}

7.2.5 json.Unmarshal関数をカスタマイズしよう

  • json.Unmarshal関数でJSONを構造体に変換する処理を独自にカスタマイズする場合、UnmarshalJSONメソッドを実装する。
import (
	"encoding/json"
	"fmt"
)

type T struct {
}

type Person struct {
	Name      string   `json:"name,omitempty"`
	Age       int      `json:"age,omitempty"`
	Nicknames []string `json:"nicknames,omitempty"`
	T         *T       `json:"T,omitempty"`
}

func (p *Person) UnmarshalJSON(b []byte) error {
	type Person2 struct {
		Name string
	}
	var p2 Person2
	err := json.Unmarshal(b, &p2)
	if err != nil {
		fmt.Println(err)
	}
	p.Name = p2.Name
	return err
}

func main() {
	b := []byte(`{"name":"Mike", "age":20, "nicknames":[]}`)
	var p Person
	if err := json.Unmarshal(b, &p); err != nil {
		fmt.Println(err)
	}
	fmt.Println(p.Name, p.Age, p.Nicknames)

	v, _ := json.Marshal(p)
	fmt.Println(string(v))
}
// 実行結果
// Mike 0 []
// {"name":"Mike"}

7.3 データベースを利用しよう

SQLiteを例に簡単なリレーショナルデータベースとSQLの使い方を記載する。

7.3.1 SQLiteを利用する準備をしよう

  • Goを使ってデータベースにアクセスする。今回はSQLiteというデータベースのシステムを利用するため、 go-sqlite3というパッケージを使う。
  • 「go get github.com/mattn/go-sqlite3」でインストールする。

SQLでデータベースを操作するには、database/sqlパッケージを利用する。データベースを操作する流れは次の通り。

  1. sql.Open関数でデータベースを開く
  2. SQL文を作る
  3. SQL文を実行する
  4. Closeメソッドでデータベースを閉じる
import (
	"database/sql"
	"log"

	_ "github.com/mattn/go-sqlite3"
)

var DbConnection *sql.DB

func main() {
	DbConnection, _ := sql.Open("sqlite3", "./example.sql")
	defer DbConnection.Close()
	cmd := `CREATE TABLE IF NOT EXISTS person(
		name STRING,
		age INT
	)`

	_, err := DbConnection.Exec(cmd)
	if err != nil {
		log.Fatalln(err)
	}
}
  • INSERT文でレコードを挿入しよう
    • personテーブルに対して、レコードを挿入するSQL文であるINSERT文を実行する。
    • 追加する値を「?」とすることで、Execメソッドの実行時に値を渡すことができる。
      • 「?」のことをブレースホルダという。
import (
	"database/sql"
	"log"

	_ "github.com/mattn/go-sqlite3"
)

var DbConnection *sql.DB

func main() {
	DbConnection, _ := sql.Open("sqlite3", "./example.sql")
	defer DbConnection.Close()
	cmd := `CREATE TABLE IF NOT EXISTS person(
		name STRING,
		age INT
	)`

	_, err := DbConnection.Exec(cmd)
	if err != nil {
		log.Fatalln(err)
	}

	cmd = "INSERT INTO person (name, age) VALUES (?, ?)"
	_, err = DbConnection.Exec(cmd, "Nancy", 20)

	if err != nil {
		log.Fatalln(err)
	}
}
  • UPDATE文でレコードを更新しよう
import (
	"database/sql"
	"log"

	_ "github.com/mattn/go-sqlite3"
)

var DbConnection *sql.DB

func main() {
	DbConnection, _ := sql.Open("sqlite3", "./example.sql")
	defer DbConnection.Close()
	cmd := `CREATE TABLE IF NOT EXISTS person(
		name STRING,
		age INT
	)`

	_, err := DbConnection.Exec(cmd)
	if err != nil {
		log.Fatalln(err)
	}

	cmd = "UPDATE person SET age = ? WHERE name = ?"
	_, err = DbConnection.Exec(cmd, 25, "Mike")

	if err != nil {
		log.Fatalln(err)
	}
}
  • SELECT文で複数のレコードを取得しよう
    • SELECT文で取得したレコードを取得したレコードを得るためにはQueryメソッドを使う。
    • Queryメソッドの返り値を代入した変数rowsはdeferde「rows.Close()」を実行する必要がある。
    • 変数rowsでNextメソッドを使い、forループで1行づつ取得する。
    • Scanメソッドで「rows.scan(&p.Name, &p.Age)」として変数pのフィールドに値を入れる。
import (
	"database/sql"
	"fmt"
	"log"

	_ "github.com/mattn/go-sqlite3"
)

var DbConnection *sql.DB

type Person struct {
	Name string
	Age  int
}

func main() {
	DbConnection, _ := sql.Open("sqlite3", "./example.sql")
	defer DbConnection.Close()
	cmd := `CREATE TABLE IF NOT EXISTS person(
		name STRING,
		age INT
	)`

	_, err := DbConnection.Exec(cmd)
	if err != nil {
		log.Fatalln(err)
	}

	cmd = "SELECT * FROM person"
	rows, _ := DbConnection.Query(cmd)
	defer rows.Close()
	var pp []Person
	for rows.Next() {
		var p Person
		err := rows.Scan(&p.Name, &p.Age)
		if err != nil {
			log.Println(err)
		}
		pp = append(pp, p)
	}
	for _, p := range pp {
		fmt.Println(p.Name, p.Age)
	}
}
  • SELECT文で1件のレコードを取得する
    • SELECT文で条件を指定する場合は、WHERE句の後に条件を書く。
    • レコードを1件だけ取得したいときは、QueryRowメソッドを使用する。
    • **sql.ErrNoRows()**当てはまるレコードが無いときのエラー
import (
	"database/sql"
	"fmt"
	"log"

	_ "github.com/mattn/go-sqlite3"
)

var DbConnection *sql.DB

type Person struct {
	Name string
	Age  int
}

func main() {
	DbConnection, _ := sql.Open("sqlite3", "./example.sql")
	defer DbConnection.Close()
	cmd := `CREATE TABLE IF NOT EXISTS person(
				name STRING,
				age INT)`
	_, err := DbConnection.Exec(cmd)
	if err != nil {
		log.Fatalln(err)
	}

	cmd = "SELECT * FROM person WHERE age = ?"
	row := DbConnection.QueryRow(cmd, 20)
	var p Person
	err = row.Scan(&p.Name, &p.Age)
	if err != nil {
		if err == sql.ErrNoRows {
			log.Println("No row")
		} else {
			log.Println(err)
		}
	}
	fmt.Println(p.Name, p.Age)
}
  • DELETE文でレコードを削除しよう
    • DELETE文でレコードを削除する。
import (
	"database/sql"
	"log"

	_ "github.com/mattn/go-sqlite3"
)

var DbConnection *sql.DB

type Person struct {
	Name string
	Age  int
}

func main() {
	DbConnection, _ := sql.Open("sqlite3", "./example.sql")
	defer DbConnection.Close()
	cmd := `CREATE TABLE IF NOT EXISTS person(
				name STRING,
				age INT)`
	_, err := DbConnection.Exec(cmd)
	if err != nil {
		log.Fatalln(err)
	}

	cmd = "DELETE FROM person WHERE name = ?"
	_, err = DbConnection.Exec(cmd, "Nancy")
	if err != nil {
		log.Fatalln(err)
	}
}

7.4 Webアプリケーションを作成しよう

シンプルにテキストファイルをデータを読み込み、GETリクエスト、POSTリクエストを処理するWebアプリケーションを例にして
基本的なWebアプリケーションのコードを説明する。

7.4.1 テキストを編集して表示するアプリケーションを作ろう

GoでWebアプリケーションを作っていきましょう。ここでは、Goの公式ページで紹介されているアプリケーションを例に説明する。
サードパーティのWebフレームワークを使わなくてもある程度のWebアプリケーションであれば、Goの標準パッケージで作ることができる。

7.4.2 osパッケージでテキストファイルを読み書きしよう

  • Goの標準パッケージであるosパッケージを使い、テキストファイルに対して読み書きを行う処理を作っていきましょう。
  • os.WriteFile関数でファイルにデータを書き込む
    • Webページを管理する構造体Pageを作る。フィールドにはstring型のtitleとbyte配列のBodyを定義する。
    • この構造体Pageにsaveメソッドを作成する。
      • 変数filenameに構造体PageのTitleに「.txt」を足した値を入れて初期化する
      • os.WriteFile関数を実行し、変数filenameのファイルに構造体PageのBodyを入れて、「0600」というパーミッションで保存する。
import "os"

type Page struct {
	Title string
	Body  []byte
}

func (p *Page) save() error {
	filename := p.Title + ".txt"
	return os.WriteFile(filename, p.Body, 0600)
}

func main() {
	p1 := &Page{Title: "test", Body: []byte("This is a sample Page.")}
	p1.save()
}
  • os.ReadFile関数でファイルからデータを読み込もう
    • loadPage関数を作成する。
      • データベースからページの情報を読み込み処理を行うイメージ
      • loadPage関数の引数はstring型のtitleとして、戻り値は*Pageとerrorを返す。
      • 変数filenameに引数のtitleに「.txt」を加えてファイル名を代入し、os.ReadFile関数で読み込んだ内容をbodyに代入する。
      • エラーハンドリングした後、TitleとBodyに値を代入して構造体のPageのアドレスとエラーがないのでnilを返り値として返す。
import (
	"fmt"
	"os"
)

type Page struct {
	Title string
	Body  []byte
}

func (p *Page) save() error {
	filename := p.Title + ".txt"
	return os.WriteFile(filename, p.Body, 0600)
}

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
}

func main() {
	p1 := &Page{Title: "test", Body: []byte("This is a sample Page.")}
	p1.save()

	p2, _ := loadPage(p1.Title)
	fmt.Println(string(p2.Body))
}

7.4.3 Webサーバーを立ち上げよう

  • httpパッケージを使い、Webサーバーの処理を作成していきましょう
  • まずは、http.ListenAndServer関数でWebサーバーを立ち上げていきます。
    • 第一引数には、「:8080」とWebサーバーを立ち上げるポートを指定します。
      • 「:8080」のように、「:」の前に何も書かない場合は、「localhost:8080」のようにlocalhostを指定してWebページにアクセスする。
    • 第二引数は**ハンドラー(HTTPリクエストに対して応答するためのインタフェース)**を指定する
      • 今回はデフォルトを指定したいため、nilと書く。
      • 「localhost:8080」でアクセスしたとき、「404 page not found」を返す。
    • ListenAndServer関数は、サーバーで問題が発生した場合にエラーを返すので
      • log.Fatal関数の引数に指定して、サーバーの実行中にエラーが起きたら出力するようにする。
    • このWebサーバーを立ち上げる前に、http.HandlerFunc関数でURLを登録する。
      • 第一引数に「/view/」を指定する、このURLにアクセスが来たときに、第二引数のハンドラーで処理を行う。
      • viewHandler関数のコードを作成する
        • 引数は(w http.ResponseWriter, r *http.Request)のように指定する。
import (
	"fmt"
	"log"
	"net/http"
	"os"
)

type Page struct {
	Title string
	Body  []byte
}

func (p *Page) save() error {
	filename := p.Title + ".txt"
	return os.WriteFile(filename, p.Body, 0600)
}

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
}

func viewHandler(w http.ResponseWriter, r *http.Request) {
	// /view/test
	title := r.URL.Path[len("/view/"):]
	p, _ := loadPage(title)
	fmt.Fprintf(w, "<h1>%s</h1><div>%s</div>", p.Title, p.Body)
}

func main() {
	http.HandleFunc("/view/", viewHandler)
	log.Fatal(http.ListenAndServe(":8080", nil))
}

7.4.4 HTMLのテンプレートを利用しよう

  • HTMLが長くなった場合、別のファイルに書くことが現実的である。
    • テンプレートとなるHTMLファイルを作り、text/templateパッケージを使って読み込んでいく。
  • viewページを作成しよう
    • 「view.html」というHTMLファイルを作成する
    • viewHandler関数の処理を変更する
      • fmt.Fprintf関数でHTMLを作成するではなく、renderTemplateという独自の関数を作成し、変数wと文字列「view」、変数pを渡してHTTPレスポンスを作成する。
    • renderTemplate関数では
      • template.ParseFiles関数でテンプレートとなるHTMLファイル作成し、テンプレートを読み込む
      • 読み込んだテンプレートを代入した変数tでExcuteメソッドを実行し、変数pに格納されたWebページの情報をhttp.ResponseWriterに書き込む。
    • view.htmlの中身を作成する
      • {{.Title}}

        」と書くと、Excuteメソッドで渡した変数pのtitleをh1要素に入れることができる。
func viewHandler(w http.ResponseWriter, r *http.Request) {
	// /view/test
	title := r.URL.Path[len("/view/"):]
	p, _ := loadPage(title)
	// fmt.Fprintf(w, "<h1>%s</h1><div>%s</div>", p.Title, p.Body)
	renderTemplate(w, "view", p)
}

func renderTemplate(w http.ResponseWriter, temp string, p *Page) {
	t, _ := template.ParseFiles(temp + ".html")
	t.Execute(w, p)
}
<h1>{{.Title}}</h1>

<p>[<a href="/edit/{{.Title}}">Edit</a>]</p>

<div>{{printf "%s" .Body}}</div>
⚠️ **GitHub.com Fallback** ⚠️