23
20

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

golang で HTTP Server を停止する方法

Last updated at Posted at 2015-06-08

個人的に嵌ったので、覚え書きです。
間違っていたら教えて下さい…。

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

参考

23
20
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
23
20

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?