個人的に嵌ったので、覚え書きです。
間違っていたら教えて下さい…。
Go を始めてから2週間ほど経ちました。
Go を書いていると心が満たされます。
問題
http.Server
さて、 Go 言語で最もシンプルな HTTP Server の実装は以下の通りだと思います。
package main
import (
"fmt"
"net/http"
)
func main() {
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, "Hello, world.")
})
http.ListenAndServe(":8080", nil)
}
しかし、これでは何かと不便です。
- サーバを停止する手段が提供されていません。
-
http.ListenAndServe
が処理をブロックします。
テスト
まずは、テストを書いてみましょう。
package main
import (
"io/ioutil"
"net/http"
"testing"
)
func TestServer(t *testing.T) {
go main()
resp, err := http.Get("http://localhost:8080")
if err != nil {
t.Fatal(err)
}
defer resp.Body.Close()
if resp.StatusCode != 200 {
t.Fatal("status:", resp.StatusCode)
}
respBody, err := ioutil.ReadAll(resp.Body)
if err != nil {
t.Fatal(err)
}
if string(respBody) != "Hello, world.\n" {
t.Fatal("body:", string(respBody))
}
}
こうなりますね。
テストケースが複数の場合
例えば、ここでパスが増えて、テストケース (Test*** 関数) を増やしたくなった場合を考えます。
次のように go main()
を再度書きますか?
package main
import (
"io/ioutil"
"net/http"
"testing"
)
func TestServer(t *testing.T) {
go main()
resp, err := http.Get("http://localhost:8080")
if err != nil {
t.Fatal(err)
}
defer resp.Body.Close()
if resp.StatusCode != 200 {
t.Fatal("status:", resp.StatusCode)
}
respBody, err := ioutil.ReadAll(resp.Body)
if err != nil {
t.Fatal(err)
}
if string(respBody) != "Hello, world.\n" {
t.Fatal("body:", string(respBody))
}
}
func TestServer2(t *testing.T) {
go main()
/* 略 */
}
すると、以下のエラーを吐いて死にます。
panic: http: multiple registrations for /
http.HandleFunc
に同じパスを渡したので panic を起こしました。
http.ServeMux
それでは、 http.ServeMux
を使って simple_server.go
を書き直します。
package main
import (
"fmt"
"net/http"
)
func main() {
mux := http.NewServeMux()
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, "Hello, world.")
})
http.ListenAndServe(":8080", mux)
}
これでテストは通るので、一見上手く行ったかのように思えますが、サーバのコードに例外処理を追加しましょう。
package main
import (
"fmt"
"net/http"
)
func main() {
mux := http.NewServeMux()
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, "Hello, world.")
})
err := http.ListenAndServe(":8080", mux)
if err != nil {
panic(err)
}
}
もう一度テストを走らせると、以下のエラーが吐かれます。
panic: listen tcp :8080: bind: address already in use
TestServer
関数を抜けても Goroutine (ゴルーチン) は止まらないので、同じポートを再度 listen しようとする為です。
そこで、テストケース毎にサーバを起動・停止する必要があります。
解答
サーバ
package main
import (
"fmt"
"log"
"net"
"net/http"
"os"
"os/signal"
"syscall"
)
func routes() (mux *http.ServeMux) {
mux = http.NewServeMux()
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, "Hello, world.")
})
return
}
func server(addr string) (listener net.Listener, ch chan error) {
ch = make(chan error)
listener, err := net.Listen("tcp", addr)
if err != nil {
panic(err)
}
go func() {
mux := routes()
ch <- http.Serve(listener, mux)
}()
return
}
func main() {
listener, ch := server(":8080")
fmt.Println("Server started at", listener.Addr())
// シグナルハンドリング (Ctrl + C)
sig := make(chan os.Signal)
signal.Notify(sig, syscall.SIGINT)
go func() {
log.Println(<-sig)
listener.Close()
}()
log.Println(<-ch)
}
テスト
追記: httptest.Server
を使って書き直しました。
package main
import (
"io/ioutil"
"net/http"
"net/http/httptest"
"testing"
)
func TestServer(t *testing.T) {
server := httptest.NewServer(routes())
defer server.Close()
resp, err := http.Get(server.URL)
if err != nil {
t.Fatal(err)
}
defer resp.Body.Close()
if resp.StatusCode != 200 {
t.Fatal("status:", resp.StatusCode)
}
respBody, err := ioutil.ReadAll(resp.Body)
if err != nil {
t.Fatal(err)
}
if string(respBody) != "Hello, world.\n" {
t.Fatal("body:", string(respBody))
}
}
func TestServer2(t *testing.T) {
server := httptest.NewServer(routes())
defer server.Close()
/* 略 */
}
これで、無事に HTTP Server を停止させることが出来ました。
channel (チャネル) 経由でエラーを受け取ることも出来ます。