GoでWebアプリケーションを作る（7）ーWebアプリケーション

こんにちは。

今日は「Webアプリケーション」についてです。

これまで順番に作り上げてきたGyudon型をWebアプリケーションサーバとして起動する方法を確認してもらいます。
本章までを通して、Go言語の基本的な扱い方を学習した皆さんには、最も簡単な章かもしれません。

# 準備

複数のターミナルを用い、ハンズオンいただきます。
既存のコンテナに接続し利用するため、それぞれターミナルを起動し、以下コマンドを実行ください。
VSCodeをお使いの方はターミナルを追加で起動してください。

```terminal
:# TERMINAL 1
$ docker exec -it go-tutor /bin/bash

:# TERMINAL 2
$ docker exec -it go-tutor /bin/bash
```

# httpを起動する方法

Go言語の標準パッケージ [net/http](https://pkg.go.dev/net/http)を活用するだけで起動します。
特に細かい処理に拘らず、デフォルト動作でWebアプリケーションサーバをコーディングするのであれば、呼び出す側は以下の2行だけですみます。

```go
func httphandler(w http.ResponseWriter, r *http.Request) {
	#...省略
}

func main() {
	http.HandleFunc("/", httphandler)   //どこのPathで、どんな処理をするか
	http.ListenAndServe("localhost:8080", nil) //どの接続元(ホスト名:ポート)で、サーバを起動するか
	if err != nil {
		panic(err)
	}
}
```

# はじめてのGo言語Webアプリケーション起動

では、Webアプリケーションサーバの書き方も知ってもらったので、実際にコーディングしてもらいましょう。
理由は後ほど説明しますが、本講義では、[net/http](https://pkg.go.dev/net/http)パッケージではなく、講義用の下位互換httpパッケージ(zakohttp) を参照してもらいます。

```terminal
:# TERMINAL 0
:# COPY /../6_struct/weakShop/shop/shop.go /../7_webapp/weakShop/shop/shop.go
:# COPY /../6_struct/weakShop/eaters.go /../7_webapp/weakShop/gyudon-httpd.go
:# WORKPATH /../7_webapp/weakShop/

$ cp /../6_struct/weakShop/shop/shop.go /../7_webapp/weakShop/shop/shop.go
$ cp /../6_struct/weakShop/eaters.go /../7_webapp/weakShop/gyudon-httpd.go
$ cd /../7_webapp/weakShop/
$ <お好きなエディタ> shop/shop.go
$ <お好きなエディタ> gyudon-httpd.go
$ go run gyudon-httpd.go


:# TERMINAL 1
$ curl http://localhost:8080/
:# 10秒程度待機する

```

- `/../7_webapp/weakShop/shop/shop.go`

```go
package shop

import (
	"fmt"
	"time"
	"../http"
)

type Gyudon struct {
	menu string
}

func NewGyudon() Gyudon {
	return Gyudon{
		menu: "NegitamaGyudon",
	}
}

func (self *Gyudon) Eat(w http.ResponseWriter, r *http.Request) { //引数をhttpdのセッション状態を受け取れるように追加
	if self.menu == "" {
		return
	}

	time.Sleep(time.Second * 10) //擬似食べてる時間
	fmt.Fprintf(w, "'%s'\n", self.menu) //食べた事を報告
	return
}

```
- `/../7_webapp/weakShop/gyudon-httpd.go`

```go
package main

import (
	"./shop"
	"./http"
)

func main() {
	myshop := shop.NewGyudon()
	http.HandleFunc("/", myshop.Eat)
	err := http.ListenAndServe("localhost:8080", nil)
	if err != nil {
		panic(err)
	}
}
```
## 結果
```terminal
:# TERMINAL 0
:# WORKPATH /../7_webapp/weakShop/

$ <お好きなエディタ> shop/shop.go
$ <お好きなエディタ> gyudon-httpd.go
$ go run gyudon-httpd.go


:# TERMINAL 1
$ curl http://localhost:8080/
:# 10秒程度待機する
'NegitamaGyudon'

```

## 後処理

```terminal
:# TERMINAL 0
:# Ctrl + C で、gyudon-httpd.goをKillしてください

```

# Goroutineに触れる

Go言語の特徴でも述べた通り、Go言語では並行処理が簡単にかけます。
最後に、本講義で書いたWebアプリケーションサーバを用い、並行処理を体験してもらおうと思います。

突然ですが、座席が1つしかない牛丼屋に行かれたことはありますか？
執筆者は、大手牛丼チェーンの各社それなりに行きますが、今のところ座席が1つの店舗に巡り合ったことはありません。
平時の店舗で、1つの座席しか無いようでは、Aさんが食べ終わるまでBさんが牛丼を食べることができず、採算取れないからでしょうか。

ご存知の方も多いでしょうが、プログラムは、指定しない限り、処理を1つずつ順番に実行します。
そのため、1つの座席しか無い牛丼屋と同じ状況が起きます。
何も考えずに、HTTPサーバを開発すると、Aさんの画面が表示されるまで、Bさんの画面はずっと読み込み中でくるくる(待ち)になってしまいます。

めちゃくちゃ美味しい隠れた名店で、1席しか無いような状況であれば、執筆者は我慢できますが、
HTTPサーバで、他者の処理が終わらないと利用できないなんて、使えたものではありません。

## 1座席なWebアプリケーションサーバ体験

試しに、2つのリクエストを送ると、1つ目のリクエストが10秒、2つ目のリクエストが約20秒かかることがわかると思います。

```go
:# TERMINAL 0
:# WORKPATH /../7_webapp/weakShop/

$ go run gyudon-httpd.go


:# TERMINAL 1
$ time curl http://localhost:8080/
:# 10秒程度待機する


:# TERMINAL 2
$ time curl http://localhost:8080/
:# 10秒程度待機する

```

### 結果

```terminal
:# TERMINAL 0
:# WORKPATH /../7_webapp/weakShop/

$ go run gyudon-httpd.go
:# 何も出力されない場合、実行中です。続くハンズオンを実施ください


:# TERMINAL 1
$ time curl http://localhost:8080/
'NegitamaGyudon'
:# 10秒程度待機する

real    0m10.036s
user    0m0.014s
sys     0m0.013s

:# TERMINAL 2
$ time curl http://localhost:8080/
'NegitamaGyudon'
:# 10秒程度待機する

real    0m18.991s
user    0m0.004s
sys     0m0.010s
```

これは、2つ目のリクエストが、1つ目のリクエストが終わるまでの待ち時間＋自身の実行時間となるためです。

## 並行プログラミング

AさんとBさんに、同時に牛丼を食べてもらう方法は、簡単です。
座席を2つ用意すれば良いのです。Cさん、Dさん.....と1000000人来店したら、来店と同時に座席を増やしてしまえば、誰も待たなくて良くなります。
これによりDさんは、AさんBさんCさんが食べ終わるのを待つことなく、食べ始めることができます。
(現実では難しいでしょうが、不思議なポッケで叶えてもらったと思ってください。)

プログラムでも、複数の処理をおおむね同時に実行する(厳密には、したように見せる。が正しい) 並行プログラミングがあります。
並行プログラミングなコードを自身で作成するためには、領域の管理を考えた数十行のコードを作成する必要があります。
牛丼屋の例で考えると、座席をどう確保すべきか、どういった要素が必要か。客が離れたら座席をどのように撤去するべきか。1つしかない厨房から牛丼をどのような形で提供するか。辺りです。

既に特徴で紹介していますが、Go言語では、Goroutine を用いることで、とても低コストに並行プログラミングを行えます。

### 🚀 Goroutineだけではなく、本講義では触れないChannelや、GCによって、低コストに並行プログラミングができます。

実際に、食べる処理(Eat関数) を、並行プログラミングにして、2つ目のリクエストが、1つ目のリクエストを待たなくても良いように、アップグレードしましょう

## 並行動作するWebアプリケーションサーバ体験

`/../7_webapp/weakShop/http/zakohttp.go` の、`c.serve(self.ctx)` が、Eat関数を呼び出しています。
ここをGoroutine化し、その先にあるEat関数を新しく誕生させた座席(Goroutine)で動作させるようにしましょう。

```terminal
:# TERMINAL 0
:# WORKPATH /go/src/go_tutorial/7_webapp/weakShop/

$ <お好きなエディタ> http/zakohttp.go
$ go run gyudon-httpd.go
:# 何も出力されない場合、実行中です。続くハンズオンを実施ください


:# TERMINAL 1
$ time curl http://localhost:8080/
:# 10秒程度待機する

:# TERMINAL 2
$ time curl http://localhost:8080/
:# 10秒程度待機する
```

- `/../7_webapp/weakShop/http/zakohttp.go`

```go
c.serve(self.ctx)    //Line56 もともとの書かれ方
go c.serve(self.ctx) //Line56 変更後。go と、加筆する
```

### 結果

```go
:# TERMINAL 0
:# WORKPATH /../7_webapp/weakShop/

$ <お好きなエディタ> http/zakohttp.go
$ go run gyudon-httpd.go
:# 何も出力されない場合、実行中です。続くハンズオンを実施ください


:# TERMINAL 1
$ docker exec -it go-tutor /bin/bash
$ time curl http://localhost:8080/
:# 10秒程度待機する
'NegitamaGyudon'


real    0m10.033s
user    0m0.019s
sys     0m0.009s

:# TERMINAL 2
$ docker exec -it go-tutor /bin/bash
$ time curl http://localhost:8080/
:# 10秒程度待機する
'NegitamaGyudon'


real    0m10.012s
user    0m0.019s
sys     0m0.009s
```

### Tips: Goroutineは、注意して使いましょう

お手軽な`go func(){}()`ですが、注意が必要です。
並行動作を簡単に行えますが、並行動作に対応した処理やデータの安全性は、プログラマ自身が考え、コード化しておく必要があります。
Goroutineが迷子になったり、データが壊れたり、リソースの奪い合いになったりと、危険なことが多くあります。
本番サービスとして、きちんと提供する場合は、並行プログラミングを学習してから利用することをお勧めします。

# zakohttpパッケージについて

こちらは、Go言語標準パッケージ net/http (opens new window)の以下の処理を参考に作成しています。

実は、本講義を通して、Go標準パッケージの劣化版を、Go標準パッケージに近づけるコーディングをしてもらいました。
[net/http](https://go.dev/src/net/http/)パッケージは、通信に関する処理や、同期処理、書き込み処理やハンドラの登録など、Goで触っておくと良さそうな表現が色々と存在します。
腕試しをされるのであれば、zakohttpの問題を考え、アップグレードし続けてみてください。

今日は以上です。
ありがとうございました。

次のPart 8は「テスト」です。
これまで作り上げてきたGyudon型にテストを追加してもらいます。

よろしくお願いいたします。