🐕

Go言語を基礎から徹底的に叩き込む〜#4-3 Web開発基礎〜

2024/04/29に公開

概要

この記事の続きです!(#4-2
Go での Web 開発の基礎についてです!

Web 開発基礎

Go で Web 開発する場合、Echo や Gin などのフレームワーク(ライブラリ)が使用されることが多いと思いますが、今回は標準パッケージに含まれる機能を主に見ていこうと思います!

net/url

URL をパースする。URL に問題があればここで検知できる。

base, err := url.Parse("https://example.com")
if err != nil {
	// エラー処理
}
reference, err := url.Parse("/test?a=1&b=2")
if err != nil {
	// エラー処理
}
endpoint := base.ResolveReference(reference).String()
fmt.Println(endpoint) // https://example.com/test?a=1&b=2

net/http

net/http パッケージのみで簡単な Web アプリケーションを実装することができる。
多くの Web フレームワーク(Echo や Gin など)も内部的にはこのパッケージを使っている。

リクエスト送信

以下は、とあるページに 単純に Get メソッドでリクエストを送信する例

resp, err := http.Get("http://example.com")
if err != nil {
	log.Fatalln(err)
}
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)
fmt.Println(string(body))

リクエスト送信についても詳細を見ていくと、Body や Header に値を含めたり、Post で送信したりと様々あるが、Web 開発の際にそこまで使用されることはないので、一旦、リクエストはここまでで割愛する。

ルーティング

HandleFuncHandle)を使って、リクエストハンドラを設定できる。
以下のとおり、第一引数にルーティングのパス、第二引数に関数をとる。関数の第一引数wはレスポンスを書き込むための io.Writer。第二引数のrはリクエスト情報が格納されている。

func main() {
	http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
		fmt.Fprintln(w, "Hello World!")
	})
	http.ListenAndServe(":8080", nil)
}

HandleFuncHandleは引数に関数を取るかどうかだけで、やっていることは同じ。
以下はどちらも同義である。

http.HandleFunc("/handle/func", func(w http.ResponseWriter, r *http.Request) {
	fmt.Fprintln(w, "HandleFunc")
})

http.Handle("/handle", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
	fmt.Fprintln(w, "Handle")
}))
http.ListenAndServe(":8080", nil)

HTTP メソッドによるハンドラの登録

GETPOSTなどのメソッドごとにハンドラを登録することはできない。
GETPOSTを区別してハンドラを設定したい場合は以下のようにswitch文で記述する必要がある。

http.HandleFunc("/handle/func", func(w http.ResponseWriter, r *http.Request) {
	switch r.Method {
	case http.MethodGet:
		fmt.Fprintln(w, "get handle func")
	case http.MethodPost:
		fmt.Fprintln(w, "post handle func")
	default:
		fmt.Fprintln(w, "invalid method")
	}
})
http.ListenAndServe(":8080", nil)

メソッドごとのハンドラの登録は Echo などの Web フレームワークを使用することで簡潔に記述できるようになる。

html/template

標準で用意されているテンプレートエンジン。
ただ、Go が Web アプリーション開発で使用される場合は、 API としての用途が多いため、今回は省略。

encoding/json

json 形式のリクエストを Go のコードで使用できるよう構造体の形式にしたり、またその逆をしたりすることをマーシャル(Marshal)、アンマーシャル(Unmarshal)という。
C#などでは、シリアライズ、デシリアライズと言われたりする。
ネットワーク越しにデータのやり取りをする際に頻発する。

マーシャル(Marshal)

マーシャルは Go の構造体データを json 形式にすること。

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

func main() {
	p := Person{
		Name:      "mike",
		Age:       20,
		Nicknames: []string{"mi", "mk", "m"},
	}
	v, _ := json.Marshal(p)
	fmt.Println(string(v)) // {"Name":"mike","Age":20,"Nicknames":["mi","mk","m"]}
}

json 形式にしたい際にプロパティ名をロワーケースにしたり、スネークケースにしたい場合、構造体に以下のような定義をする。

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

func main() {
	p := Person{
		Name:      "mike",
		Age:       20,
		Nicknames: []string{"mi", "mk", "m"},
	}
	v, _ := json.Marshal(p)
	fmt.Println(string(v)) //{"name":"mike","age":20,"nick_names":["mi","mk","m"]}
}

アンマーシャル(Unmarshal)

アンマーシャルは json で入ってきたデータを Go の構造体の形式にすること。

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

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

空の場合、項目ごと削除する

以下のようにomitemptyを付与することで値が空の場合は、項目ごと消してくれる。
以下の場合、nameは空の文字列のため、マーシャルのタイミングで項目ごと削除される。

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

func main() {
	p := Person{
		Name:      "",
		Age:       20,
		Nicknames: []string{"mi", "mk", "m"},
	}
	v, _ := json.Marshal(p)
	fmt.Println(string(v)) //{"age":20,"nick_names":["mi","mk","m"]}
}

ただし、以下注意点として、自分で定義した構造体の場合、ポインタ型にしないとomitemptyが効かない。

type T struct{}

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

func main() {
	p := Person{
		Name:      "",
		Age:       20,
		Nicknames: []string{"mi", "mk", "m"},
	}
	v, _ := json.Marshal(p)
	fmt.Println(string(v)) // {"age":20,"nick_names":["mi","mk","m"],"t":{}}
}

(Person の T は omitempty が効かず、空の項目として含まれてしまっている。ポインタ型だと問題なく効く)

マーシャルとアンマーシャルをカスタマイズする。

自分が独自に定義した形式でマーシャルとアンマーシャルをしてほしい場合は、カスタマイズすることができる。
Stringer の String メソッドと同様にMarshalJSONUnmarshalJSONは特別なメソッドと覚えておくと良い。

マーシャルをカスタマイズした例

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

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

func main() {
	p := Person{
		Name:      "Mike",
		Age:       20,
		Nicknames: []string{"mi", "mk", "m"},
	}
	v, _ := json.Marshal(p)
	fmt.Println(string(v)) //{"Name":"Mr.Mike"}
}

アンマーシャルをカスタマイズした例

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) //{mike! 0 []}
}

context

キャンセルやタイムアウトを提供するパッケージ。
API を実装している場合、net/http パッケージの*http.RequestContextメソッドから取得した値を利用する。
自作のハンドラーメソッド内で利用しなくてもcontext.Context型の値は受け取れるようにしておくべき。そうしないとキャンセルを伝搬できないから。(現在の多くのパッケージで定義されている関数やメソッドは第一引数にcontext.Context型の値を受け取るようになっている。)

以下は例

func HandleOriginalFunc(w http.ResponseWriter, r *http.Request) {
	b, err := GetBook(r.Context(), 1)

}

func GetBook(ctx context.Context, id int) (*Book, error) {
	rows, err := db.QueryContext(ctx, "SELECT ....省略")
}

以下は、タイムアウトした場合に処理をキャンセルする例

func longProcess(ctx context.Context, ch chan string) {
	fmt.Println("開始")
	time.Sleep(2 * time.Second)
	fmt.Println("終了")
	ch <- "実行結果"
}

func main() {
	ch := make(chan string)
	ctx := context.Background()
	ctx, cancel := context.WithTimeout(ctx, 1*time.Second)
	defer cancel()

	go longProcess(ctx, ch)
L:
	for {
		select {
		case <-ctx.Done():
			fmt.Println("Error")
			fmt.Println(ctx.Err())
			break L
		case s := <-ch:
			fmt.Println(s)
			fmt.Println("success")
			break L
		}
	}
}
  • ctx := context.Background()で context の作成
  • ctx, cancel := context.WithTimeout(ctx, 1*time.Second) でタイムアウトの時間を設定
    上記の例では、タイムアウトを 1 秒間に設定しており、longProcessで 2 秒以上かかるので、処理が中断され、for 文に入るという流れになる。

database/sql パッケージ

DB を操作するための標準パッケージ

sql.Open 関数

sql.Open 関数はプログラム起動時に一度のみ実行すれば OK となります。
理由は、database/sql パッケージにはコネクションをプールしておく機能があるため、アプリケーションが終了するまで Cloese する必要はないからです!

sql.ErrorNoRows

sql.ErrorNoRows エラーとは、レコードが取得できなかった時に発生するエラーですが、これは*sql.Row型が戻り値のメソッドのみ発生します。
sql.QueryRow.Scan でスキャンできるレコードがないため、発生するエラーであるため、以下のような場合は発生しません。

rows, err := r.db.QueryContext(ctx, "SELECT name FROM users WHERE age=?", age)
if errors.Is(err, sql.ErrNoRows){
	//この条件を満たすことはない
}

この例からも分かるように注意しておくべきことは、そのメソッドは本当にsql.ErrNoRowsを返すのか?ということです。

トランザクション

トランザクションを使う時は、defer文で処理を抜ける時に必ずRollbackされるようにするのがベストプラクティスとなります。
理由としては、エラーハンドリングごとにRollbackを記述していると漏れる可能性があるためです。
そのため、トランザクション内で問題なく処理が成功した場合は、Commitされたのち、Rollbackが走ることになるが、これは特に問題にはならない(Rollback メソッドは Commit 済みのトランザクションに対して実行されても RDBMS 上で実行されることはない)。それより、問題があった場合に、Rollbackを忘れる方が問題であるため、defer文で必ず実行されるようにしておきましょう。

まとめ

ざっと標準パッケージにおける Web 開発の基本的なパッケージを見ていきました。
次回は、クリーンアーキテクチャについて簡単に見ていきます。

GitHubで編集を提案

Discussion