Go言語を基礎から徹底的に叩き込む〜#4-3 Web開発基礎〜
概要
この記事の続きです!(#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 開発の際にそこまで使用されることはないので、一旦、リクエストはここまでで割愛する。
ルーティング
HandleFunc
(Handle
)を使って、リクエストハンドラを設定できる。
以下のとおり、第一引数にルーティングのパス、第二引数に関数をとる。関数の第一引数w
はレスポンスを書き込むための io.Writer。第二引数のr
はリクエスト情報が格納されている。
func main() {
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, "Hello World!")
})
http.ListenAndServe(":8080", nil)
}
HandleFunc
とHandle
は引数に関数を取るかどうかだけで、やっていることは同じ。
以下はどちらも同義である。
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 メソッドによるハンドラの登録
GET
やPOST
などのメソッドごとにハンドラを登録することはできない。
GET
やPOST
を区別してハンドラを設定したい場合は以下のように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 メソッドと同様にMarshalJSON
とUnmarshalJSON
は特別なメソッドと覚えておくと良い。
マーシャルをカスタマイズした例
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.Request
のContext
メソッドから取得した値を利用する。
自作のハンドラーメソッド内で利用しなくても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 開発の基本的なパッケージを見ていきました。
次回は、クリーンアーキテクチャについて簡単に見ていきます。
Discussion