こんにちは。
今日は「Webアプリケーション」についてです。
これまで順番に作り上げてきたGyudon型をWebアプリケーションサーバとして起動する方法を確認してもらいます。
本章までを通して、Go言語の基本的な扱い方を学習した皆さんには、最も簡単な章かもしれません。
準備
複数のターミナルを用い、ハンズオンいただきます。
既存のコンテナに接続し利用するため、それぞれターミナルを起動し、以下コマンドを実行ください。
VSCodeをお使いの方はターミナルを追加で起動してください。
:# TERMINAL 1
$ docker exec -it go-tutor /bin/bash
:# TERMINAL 2
$ docker exec -it go-tutor /bin/bash
httpを起動する方法
Go言語の標準パッケージ net/httpを活用するだけで起動します。
特に細かい処理に拘らず、デフォルト動作でWebアプリケーションサーバをコーディングするのであれば、呼び出す側は以下の2行だけですみます。
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パッケージではなく、講義用の下位互換httpパッケージ(zakohttp) を参照してもらいます。
:# 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
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
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 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 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秒かかることがわかると思います。
:# 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 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 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
c.serve(self.ctx) //Line56 もともとの書かれ方
go c.serve(self.ctx) //Line56 変更後。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パッケージは、通信に関する処理や、同期処理、書き込み処理やハンドラの登録など、Goで触っておくと良さそうな表現が色々と存在します。
腕試しをされるのであれば、zakohttpの問題を考え、アップグレードし続けてみてください。
今日は以上です。
ありがとうございました。
次のPart 8は「テスト」です。
これまで作り上げてきたGyudon型にテストを追加してもらいます。
よろしくお願いいたします。