Edited at

golang で HTTP Server を停止する方法

More than 3 years have passed since last update.

個人的に嵌ったので、覚え書きです。

間違っていたら教えて下さい…。

Go を始めてから2週間ほど経ちました。

Go を書いていると心が満たされます。


問題


http.Server

さて、 Go 言語で最もシンプルな HTTP Server の実装は以下の通りだと思います。


simple_server.go

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 が処理をブロックします。


テスト

まずは、テストを書いてみましょう。


simple_server_test.go

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() を再度書きますか?


simple_server_test.go

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 を書き直します。


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)
}


これでテストは通るので、一見上手く行ったかのように思えますが、サーバのコードに例外処理を追加しましょう。


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.")
})
err := http.ListenAndServe(":8080", mux)
if err != nil {
panic(err)
}
}


もう一度テストを走らせると、以下のエラーが吐かれます。

panic: listen tcp :8080: bind: address already in use

TestServer 関数を抜けても Goroutine (ゴルーチン) は止まらないので、同じポートを再度 listen しようとする為です。

そこで、テストケース毎にサーバを起動・停止する必要があります。


解答


サーバ


simple_server.go

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 を使って書き直しました。


simple_server_test.go

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 (チャネル) 経由でエラーを受け取ることも出来ます。


参考