Menu

Go言語のHTTPサーバー:net/httpのルーティング、JSON、ステータスコード

Goの標準のnet/httpパッケージでWebサーバーを作る方法を解説します。ハンドラ、メソッドとパスのワイルドカードを使うServeMuxのルーティング(Go 1.22)、JSONレスポンス、ステータスコード、ミドルウェア、タイムアウト、グレースフルシャットダウンまで。

このページのコードはエディタで実行できます - 編集してすぐに結果を確認できます。

数行で書くサーバー

ハンドラは、リクエストを受け取ってレスポンスを書く関数です。ServeMux がリクエストをハンドラに振り分けます。

エディタはブラウザからの接続を受け付けられないので、このページの例は httptest.NewServer でサーバーを開始し、同じプログラムから呼び出しています。実際のプログラムでは、最後の部分がブロックして永遠にサービスを提供する1行に置き換わります。

log.Fatal(http.ListenAndServe(":8080", mux))

すると curl localhost:8080/hello/gopherHello, gopher! と表示します。ListenAndServe はエラー(たとえばポートが使用中)のときにしか戻らないので、log.Fatal で包んでいます。

各リクエストは専用のゴルーチンで実行されます。マップやカウンタなど、ハンドラ間で共有するものにはミューテックスが必要です。

ハンドラ

ServeHTTP(http.ResponseWriter, *http.Request) メソッドを持つものはすべて http.Handler です。http.HandlerFunc は普通の関数をそのインターフェースに合わせるもので、mux.HandleFunc はその変換をやってくれます。ハンドラが依存関係を必要とするときは、構造体のハンドラが便利です。

type API struct {
	db *sql.DB
}

func (a *API) listItems(w http.ResponseWriter, r *http.Request) { /* uses a.db */ }

mux.HandleFunc("GET /items", api.listItems)

*http.Request からは次のものが得られます。

フィールドやメソッド内容
r.MethodGETPOST など
r.URL.Pathパス、/items/42
r.PathValue("id")ルートのパターンのワイルドカード(Go 1.22)
r.URL.Query().Get("q")クエリ文字列のパラメータ
r.Header.Get("Authorization")リクエストヘッダー
r.Bodyリクエストボディ、io.ReadCloser(サーバーが閉じる)
r.FormValue("name")フォームのフィールドかクエリパラメータ
r.Context()クライアントが切断するとキャンセルされるコンテキスト

ルーティングのパターン(Go 1.22)

Go 1.22以降、ServeMux のパターンは [METHOD ][HOST]/[PATH] の形を取り、パスにワイルドカードを含められます。

パターン一致するもの
"/items/"/items/ とその下のすべて(末尾のスラッシュ=接頭辞)
"/items"/items だけ
"GET /items/{id}"/items/42 への GET(と HEAD)。r.PathValue("id") == "42"
"POST /items"/items への POST だけ
"/files/{path...}"/files/a/b/cpath"a/b/c"
"/{$}"すべてのパスではなく / だけ
"/"他のどのパターンにも一致しないすべてのパス

2つのパターンが一致するときは、より具体的なほうが勝つので、/items/new/items/{id} に勝ちます。どちらもより具体的でない場合、たとえば /items/{id}/{kind}/new(どちらも /items/new に一致)では、2つ目を登録すると両方のパターン名を示すメッセージとともにpanicします。パスは一致したがメソッドが一致しない場合、muxは何もコードを書かなくても Allow ヘッダー付きで 405 Method Not Allowed を返します。

"/" のパターンは何でも受け止めるものです。ホームページを "/" で登録して、未知のURLすべてに200で応答していることに気づき、驚く人がいます。ホームページには "GET /{$}" を使います。

これらのパターンには、go.modgo 1.22 以降が必要です。それより古いバージョンの行だと、muxは古い挙動に戻ります。"GET /items" をホスト名とそれに続くパスとして読むので、ルートは決して一致せず、/items へのすべてのリクエストが404になります。

小さなJSON API

最後のリクエストは、どのルートも受け付けない DELETE を使っており、muxは自分で Allow: GET, HEAD 付きの 405 を返します。これはそのパスに登録されたメソッドです(GET のルートは HEAD も受け付けます)。

ステータスコードと書き込みの順序

レスポンスには3つの部分があり、ヘッダー、ステータス、ボディの順で書く必要があります。

  1. w.Header().Set(...) はヘッダーを変更します。効果があるのはステータスが送られるまでです。
  2. w.WriteHeader(code) はステータス行とヘッダーを送ります。
  3. w.Write(...)(または fmt.Fprint(w, ...) やエンコーダー)はボディを送ります。WriteHeader が呼ばれていなければ、最初の Write が先に 200 OK を送ります。

その結果、次のことが起きます。

  • ボディが始まった後にヘッダーを設定しても何も起きません。
  • WriteHeader を2回呼ぶと http: superfluous response.WriteHeader call とログに出て、最初のステータスが保たれます。
  • エラーのレスポンスの後は return します。http.Error はハンドラを止めないので、その後のコードは同じレスポンスに書き込み続けます。

生の数値ではなく名前付きの定数(http.StatusOKhttp.StatusCreatedhttp.StatusBadRequesthttp.StatusUnauthorizedhttp.StatusNotFoundhttp.StatusInternalServerError)を使います。http.StatusText(404)"Not Found" を返します。

ミドルウェア

ミドルウェアは、ハンドラを受け取ってハンドラを返し、次のハンドラを呼ぶ前後に何かをする関数です。

同じリクエストについて、log: の行は常に client got: より前に現れます。ハンドラのゴルーチンは戻る前にそれを表示し、サーバーはハンドラが戻るまでレスポンスを終えないからです。

statusRecorder は本物の ResponseWriter を埋め込み、WriteHeader だけを上書きしています。ミドルウェアからステータスコードを観察する標準的な方法です。

本番用の設定

http.ListenAndServe はタイムアウトのないサーバーを使うので、遅いクライアントが接続をいつまでも開いたままにできます。インターネットに公開するものでは http.Server を明示的に設定し、グレースフルにシャットダウンします。

srv := &http.Server{
	Addr:              ":8080",
	Handler:           mux,
	ReadHeaderTimeout: 5 * time.Second,
	ReadTimeout:       10 * time.Second,
	WriteTimeout:      30 * time.Second,
	IdleTimeout:       120 * time.Second,
}

go func() {
	if err := srv.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
		log.Fatal(err)
	}
}()

ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
defer stop()
<-ctx.Done() // wait for Ctrl+C or a SIGTERM from the orchestrator

shutdownCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
if err := srv.Shutdown(shutdownCtx); err != nil { // finish in-flight requests
	log.Println("shutdown:", err)
}

Shutdown は新しい接続の受け付けを止め、コンテキストの期限まで、処理中のリクエストが終わるのを待ちます。ListenAndServeShutdown が始まるとすぐに http.ErrServerClosed を返すので、そのエラーは失敗として扱いません。長く動くハンドラでは、クライアントがいなくなったら止まるよう r.Context() を下に渡します。

静的ファイルの配信は1行です:mux.Handle("GET /static/", http.StripPrefix("/static/", http.FileServer(http.Dir("public"))))

よくある間違い

  • http.Error の後にreturnしない。 ハンドラは動き続け、レスポンスにさらに書き込みます。
  • ボディを書いた後にヘッダーを設定する。 何も言わずに捨てられます。
  • ホームページを "/" に登録する。 未知のすべてのパスの受け皿になります。"/{$}" を使います。
  • ロックなしでハンドラ間の状態を共有する。 リクエストは並行に実行されます。
  • 本番でデフォルトのサーバーを使う。 タイムアウトがありません。http.Server で設定します。
  • ルーティングのパターンが無視される。 "GET /x/{id}" が決して一致しないなら、go.modgo 1.22 以降を指定しているか確認します。

よくある質問

Goで簡単なWebサーバーを作るには?

ハンドラを登録して待ち受けを始めます:http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { fmt.Fprintln(w, "hello") }) の後に log.Fatal(http.ListenAndServe(":8080", nil))。標準ライブラリのサーバーは本番品質で、HTTP/1.1、TLS上のHTTP/2、キープアライブ、接続ごとに1つのゴルーチンを扱います。

Goのnet/httpでパスパラメータを取得するには?

Go 1.22以降、ServeMux のパターンにはワイルドカードを含められます。mux.HandleFunc("GET /items/{id}", h) と登録し、ハンドラの中で r.PathValue("id") で値を読みます。末尾の {path...} はパスの残りに一致します。1.22より前は、r.URL.Path を自分で分割するか、chiのようなルーターを使う必要がありました。

GoでREST APIを作るのにGinのようなフレームワークは必要ですか?

必要ありません。Go 1.22以降、net/http はメソッドとパスパラメータでルーティングでき、ルーターが使われていた主な理由をまかなえます。ボディには encoding/json を、ログや認証には小さなミドルウェア関数を使えば、ほとんどのAPIは標準ライブラリで足ります。フレームワークはリクエストのバインドや検証などの便利な機能を加えるものです。

GoのHTTPハンドラでステータスコードを設定するには?

ボディを書く前に w.WriteHeader(http.StatusCreated) を呼びます。先にボディを書くと、Goは自動的に 200 OK を送り、後の WriteHeadersuperfluous response.WriteHeader call というログとともに無視されます。ヘッダーも WriteHeader の前に w.Header().Set で設定します。エラーには、http.Error(w, msg, code) がそのすべてをやってくれます。

Coddy programming languages illustration

Coddyでコードを学ぼう

始める