Select
2つのURLを取得し、それらをHTTP GETでヒットして最初に返されたURLを返すことで「競合」するWebsiteRacerと呼ばれる関数を作成するように求められました。 10秒以内に戻らない場合は、「エラー(error)」を返します。
これには
- HTTP呼び出しを行うための net/http
- ゴルーチン
- プロセスを同期するためのselect
最初にテストを書く
func TestRacer(t *testing.T){
slowURL := "http://www.facebook.com"
fastURL := "http://www.quii.co.uk"
want := fastURL
got := Racer(slowURL, fastURL)
if got != want {
t.Errorf("got %q, want %q", got, want)
}
}
テストを実行すると./racer_test.go:14:9: undefined: Racer
で失敗します。
テストを実行するための最小限のコードを記述し、失敗したテスト出力を確認します
先ほどは関数の未定義でテスト実行に失敗したので、最低限の関数を用意します。
func Racer(slowURL, fastURL string) (winner string){
return
}
これでテストを実行すると main_test.go:15: got "", want "http://www.quii.co.uk"
とテストが走るようになります。
成功させるのに十分なコードを書く
では、テストが通るように修正します。
しかし、ここで問題なのはテストで本当のリクエストを飛ばすと外部サービスに依存してしまうためよくない方向です。そこで、標準ライブラリには、net/http/httptest
というパッケージがあり、模擬HTTPサーバーを簡単に作成できるものがあるため、こちらを使用します。
テストをモックを使用するように変更して、制御できる信頼性の高いサーバーをテストできるようにします。
とりあえず遅いサーバーを用意します。(冗長ですが可読性を重視しました。)
slowTestHandler := func(w http.ResponseWriter, r *http.Request) {
time.Sleep(20 * time.Millisecond) // 20ミリ秒の遅延
w.WriteHeader(http.StatusOK) // ステータスコード200を返す
}
slowHandler := http.HandlerFunc(slowTestHandler)
slowServer := httptest.NewServer(slowHandler)
これだけだと初見ではよくわからないので詳しくみてみます。
NewServerの解説
まずNewServer
の定義を見てみると以下のとおりです。
引数のhandler
はhttp.Handler
のインターフェースを実装している必要があります。
func NewServer(handler http.Handler) *Server {
ts := NewUnstartedServer(handler)
ts.Start()
return ts
}
具体的には、handler
はServeHTTP(w http.ResponseWriter, r *http.Request)
メソッドを持っている必要があります。
type Handler interface {
ServeHTTP(ResponseWriter, *Request)
}
そこでhttp.HandlerFunc
の定義を見てみると次の通りです。
type HandlerFunc func(ResponseWriter, *Request)
// ServeHTTP calls f(w, r).
func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) {
f(w, r)
}
つまり、以下のコードでは無名関数のtestHanler
をHandlerFunc
に型変換することでhttp.Handler
のインターフェースを実装していることとなります。
testHandler := func(w http.ResponseWriter, r *http.Request) {
time.Sleep(20 * time.Millisecond) // 20ミリ秒の遅延
w.WriteHeader(http.StatusOK) // ステータスコード200を返す
}
// テスト用ハンドラを定義
handler := http.HandlerFunc(testHandler)
つまり、handler.ServeHTTP(X, Y)
と呼び出した時に、testHandler(X, Y)
が呼び出されるという流れです。http.HandlerFunc
がラッパーとして活躍しているわけです。
遠回りとなりましたが、このhandlerは先述したNewServer
の中で以下のように使用され、起動したサーバーインスタンスを返す形となります。
func NewServer(handler http.Handler) *httptest.Server {
// 新しいテストサーバーインスタンスを作成します(まだ起動していない)。
ts := httptest.NewUnstartedServer(handler)
// テストサーバーを起動します。
ts.Start()
// 起動したサーバーインスタンスを返します。
return ts
}
この返却されたサーバーはServer
構造体で次のようなフィールドを保持しています。
フィールド名 | 型 | 説明 |
---|---|---|
URL | string | base URL of form http://ipaddr:port with no trailing slash |
Listener | net.Listener | ローカルループバックインターフェース、エンドツーエンドHTTPテスト用 |
EnableHTTP2 | bool | HTTP/2がサーバーで有効かどうかを制御 |
TLS | *tls.Config | オプションのTLS構成、TLS開始後に新しい構成で設定 |
Config | *http.Server | NewUnstartedServer呼び出し後、StartまたはStartTLS呼び出し前に変更可能 |
certificate | *x509.Certificate | TLS構成証明書の解析バージョン |
wg | sync.WaitGroup | サーバー上の未完了HTTPリクエスト数をカウント、Closeは全リクエスト完了までブロック |
mu | sync.Mutex | closedとconnsを保護するためのミューテックス |
closed | bool | サーバーが閉じられているかどうかを示すフラグ |
conns | map[net.Conn]http.ConnState | HTTP接続の状態を保持(端末状態を除く) |
client | *http.Client | サーバーで使用するために構成されたクライアント、Closeが呼び出されると自動的に閉じる |
残りのテストを書く
次に早いサーバーを用意し、全体を修正します。
package Select
import (
"net/http"
"net/http/httptest"
"testing"
"time"
)
func TestRacer(t *testing.T){
slowTestHandler := func(w http.ResponseWriter, r *http.Request) {
time.Sleep(20 * time.Millisecond) // 20ミリ秒の遅延
w.WriteHeader(http.StatusOK) // ステータスコード200を返す
}
slowHandler := http.HandlerFunc(slowTestHandler)
slowServer := httptest.NewServer(slowHandler)
fastTestHandler := func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
}
fastHandler := http.HandlerFunc(fastTestHandler)
fastServer := httptest.NewServer(fastHandler)
slowURL := slowServer.URL
fastURL := fastServer.URL
want := fastURL
got := Racer(slowURL, fastURL)
if got != want {
t.Errorf("got %q, want %q", got, want)
}
slowServer.Close()
fastServer.Close()
}
これでテストを実行すると、main_test.go:31: got "", want "http://127.0.0.1:50664"
で失敗します。
テストがパスするようコードを修正
以下の通り修正します。
package Select
import (
"net/http"
"time"
)
func Racer(a, b string) (winner string) {
startA := time.Now()
http.Get(a)
aDuration := time.Since(startA)
startB := time.Now()
http.Get(b)
bDuration := time.Since(startB)
if aDuration < bDuration {
return a
}
return b
}
リファクタリング
次に重複するコードがいくつかあるのでそちらをリファクタリングします。
こちらの修正ではmeasureReponseTime
関数を用意し、処理を共通化します。
package Select
import (
"net/http"
"time"
)
func Racer(a, b string) (winner string) {
aDuration := measureResponseTime(a)
bDuration := measureResponseTime(b)
if aDuration < bDuration {
return a
}
return b
}
func measureResponseTime(url string) time.Duration {
start := time.Now()
http.Get(url)
Duration := time.Since(start)
return Duration
}
次にテストでは以下の通りリファクタリングします。
サーバーインスタンスを返却する関数を作成することで共通処理をまとめることができます。また、サーバーをクローズする記述をdefer
を使用することで見通しよくなります。
場合によっては、ファイルを閉じるなどのリソースをクリーンアップする必要があります。この場合、サーバーがポートをリッスンし続けないようにサーバーを閉じる必要があります。
これを関数の最後に実行したいが、将来のコードの読む人のために、サーバーを作成した場所の近くに命令を置いておきます。
package Select
import (
"net/http"
"net/http/httptest"
"testing"
"time"
)
func TestRacer(t *testing.T){
slowServer := makeDelayedServer(20 * time.Millisecond)
fastServer := makeDelayedServer(0 * time.Millisecond)
defer slowServer.Close()
defer fastServer.Close()
slowURL := slowServer.URL
fastURL := fastServer.URL
want := fastURL
got := Racer(slowURL, fastURL)
if got != want {
t.Errorf("got %q, want %q", got, want)
}
}
func makeDelayedServer(delay time.Duration) *httptest.Server{
return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
time.Sleep(delay)
w.WriteHeader(http.StatusOK)
}),
)
}
プロセスの同期
チャネルを使用してさらにリファクタリングします。
Racer
関数では、リクエストしてから返却される具体的な時間を使用してレスポンスの速さを試していましたが、今回のどちらが早くレスポンスを返すかどうかというコンテクストでは、その時間は不要です。ただ、どちらが早いかさえ分かれば良いからです。
したがって、以下のようにチャネルを使用し書き換えることでよりシンプルな記述になります。
func Racer(a, b string) (winner string) {
select {
case <- ping(a):
return a
case <- ping(b):
return b
}
}
func ping(url string) chan struct{} {
ch := make(chan struct{})
go func(){
http.Get(url)
close(ch)
}()
return ch
}
select 文は複数のチャネル操作を同時に待機し、最初に完了した操作を実行します。
つまり、select 文内で ping(a) と ping(b) が同時に実行されるため、並行してリクエストを送信し、その完了を待つことができます。
- case <-ping(a): ping(a) が返すチャネルが閉じられるのを待ちます。チャネルが閉じられた場合、このブロックが実行され、a を返します。
- case <-ping(b): ping(b) が返すチャネルが閉じられるのを待ちます。チャネルが閉じられた場合、このブロックが実行され、b を返します。
したがって、どちらのレスポンスが早いかということを判別できるわけです。