Go実践 #1 はじめてのHTTPサーバー
Go実践シリーズのスタートです。これまでの基礎・中級・上級で見てきた道具たちが — 実際のバックエンドを書くときどう組み立てられるかを、6編にわたって整理します。
- はじめてのHTTPサーバー (この記事) — net/http、ListenAndServe、graceful shutdown
- ルーティング — Go 1.22+ ServeMuxのパターンマッチング
- JSON入出力と入力検証
- DB連携 —
database/sql、トランザクション - ミドルウェアパターン
- テストとデプロイ —
httptest、Docker
Goがバックエンドで強い最大の理由 — 標準のnet/httpだけでプロダクションサーバーが可能、という点です。この記事はその出発点です。
最小のサーバー #
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)
}go run hello.go
curl http://localhost:8080
# Hello, world!19行(空行を除けば7行)。外部依存ゼロ。Express、FastAPIのようなフレームワークの助けなしに — 標準ライブラリだけで。
3つのコア型 #
type Handler interface {
ServeHTTP(w ResponseWriter, r *Request)
}
type ResponseWriter interface {
Header() Header
Write([]byte) (int, error)
WriteHeader(statusCode int)
}
type Request struct {
Method, URL, Header, Body, ...
}Handler— すべてのリクエスト処理器の共通インターフェースResponseWriter— レスポンスの作成Request— リクエスト情報
この3つが — Go HTTPコードのアルファベットです。
HandleFunc vs Handler
#
// 関数として登録
http.HandleFunc("/a", func(w, r) { ... })
// Handlerインターフェースとして登録
type myHandler struct{}
func (h *myHandler) ServeHTTP(w, r) { ... }
http.Handle("/b", &myHandler{})ほとんどの場合 — HandleFuncが簡潔でよく使われます。状態(フィールド)が必要なハンドラは — 構造体 + ServeHTTP。
HandleFuncは — 内部的にhttp.HandlerFuncアダプタで関数をHandlerに変換します(アダプタパターンの定石)。
レスポンスの書き込み #
func handler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json") // 1) ヘッダーを先に
w.WriteHeader(http.StatusOK) // 2) ステータスコード
fmt.Fprintln(w, `{"ok":true}`) // 3) ボディ
}順序が重要です:
Header().SetはWriteHeaderまたはWriteが呼ばれる前に- そのあとにヘッダーを変えても効果なし
WriteHeaderを呼ばないと — 最初のWriteの時点で自動的に200。
URL/Queryパラメータ #
func handler(w http.ResponseWriter, r *http.Request) {
q := r.URL.Query()
name := q.Get("name") // ?name=...
page := q.Get("page")
fmt.Fprintf(w, "Hello %s on page %s", name, page)
}curl 'http://localhost:8080/?name=lee&page=2'
# Hello lee on page 2Query()が — url.Values (map[string][]string)を返します。同じキーが複数回登場しうる点に注意。
メソッド分岐 #
func handler(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case http.MethodGet:
// 取得
case http.MethodPost:
// 作成
default:
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
}
}Go 1.22+からは — ServeMux自体がメソッドマッチングをサポートします。次の記事で詳しく。
リクエストボディの読み込み #
func handler(w http.ResponseWriter, r *http.Request) {
body, err := io.ReadAll(r.Body)
if err != nil {
http.Error(w, "read failed", http.StatusBadRequest)
return
}
defer r.Body.Close()
fmt.Fprintf(w, "got %d bytes", len(body))
}r.Bodyは — io.ReadCloser。JSONのような構造化された入力は#3 JSONで詳しく。
ボディサイズの制限 #
r.Body = http.MaxBytesReader(w, r.Body, 1<<20) // 1 MiB
body, err := io.ReadAll(r.Body)悪意ある巨大なリクエストを防ぐために — 常に上限を設けるのが良いです。
ステータスコード — 標準定数 #
http.StatusOK // 200
http.StatusCreated // 201
http.StatusBadRequest // 400
http.StatusUnauthorized // 401
http.StatusNotFound // 404
http.StatusInternalServerError // 500マジックナンバーの代わりに定数で。IDEの自動補完と可読性の両方が良くなります。
http.Errorヘルパー
#
http.Error(w, "not found", http.StatusNotFound)内部的に — ヘッダー設定 + ステータスコード + メッセージを一度に。シンプルなエラーレスポンスでよく使います。
Server構造体で設定を明示 #
srv := &http.Server{
Addr: ":8080",
Handler: mux,
ReadTimeout: 5 * time.Second,
WriteTimeout: 10 * time.Second,
IdleTimeout: 120 * time.Second,
}
log.Fatal(srv.ListenAndServe())http.ListenAndServe(":8080", nil)は便利ですが — タイムアウトのデフォルト値が無限。プロダクションでは常にhttp.Serverを直接作ってタイムアウトを設定するのが安全です(slowloris攻撃の防御など)。
Graceful shutdown — 正常終了 #
サーバーを止めるためにSIGINTを送ったとき — 進行中のリクエストを終わらせて終了したい場合。
package main
import (
"context"
"errors"
"log"
"net/http"
"os/signal"
"syscall"
"time"
)
func main() {
srv := &http.Server{Addr: ":8080", Handler: handler()}
go func() {
if err := srv.ListenAndServe(); !errors.Is(err, http.ErrServerClosed) {
log.Fatal(err)
}
}()
// SIGINT/SIGTERMを待つ
ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
defer stop()
<-ctx.Done()
log.Println("shutting down...")
shutdownCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
if err := srv.Shutdown(shutdownCtx); err != nil {
log.Fatal("shutdown:", err)
}
log.Println("bye")
}流れ:
- 別ゴルーチンでListenAndServe — メインはシグナル待機
signal.NotifyContext— Go 1.16+の標準パターン。コンテキストでシグナル処理srv.Shutdown(ctx)— 進行中のリクエストが終わるまで(またはctxタイムアウト)
Kubernetes やECSのような環境で安全なロールアウトのために必須。
静的ファイル — http.FileServer
#
http.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.Dir("public"))))http.Dir("public")— ディスク上のディレクトリhttp.FileServer— そのディレクトリを配信するハンドラhttp.StripPrefix— URL prefixの除去(これを抜くとpublic/static/...を探そうとする)
小さな静的サイトは — 1行で完了。
http.HandlerFuncアダプタ
#
func myFunc(w http.ResponseWriter, r *http.Request) { ... }
var h http.Handler = http.HandlerFunc(myFunc)関数が — インターフェースのメソッド1つを満たすように変換。ミドルウェア(#5)の中核となるテクニックです。
落とし穴 — ハンドラは並行実行 #
同じハンドラが — 複数のリクエストから同時に呼ばれます。ハンドラが共有状態を触るなら — 上級#2 syncの道具が必要。
var counter int // 共有変数
func handler(w http.ResponseWriter, r *http.Request) {
counter++ // ✗ race
}解決: atomic.Int64またはMutex。
落とし穴 — レスポンス書き込み後のreturnを忘れる #
if err != nil {
http.Error(w, "...", 400)
// returnを忘れる → 下のコードも実行されてヘッダー衝突
}
fmt.Fprintln(w, "...")エラーレスポンスのあとは常にreturnで抜けてください。
まとめ #
この記事で整理した内容:
net/httpだけでプロダクションサーバーが可能- コア3型 —
Handler、ResponseWriter、Request HandleFuncが一般的、状態が必要なら構造体 + ServeHTTP- レスポンス作成の順序 — Header → WriteHeader → Write
http.Serverを直接作ってタイムアウトは常に設定- graceful shutdown —
signal.NotifyContext+srv.Shutdown(ctx) - ハンドラは並行実行 — 共有状態は同期化
次の記事(#2 ルーティング)では — Go 1.22+のServeMuxがどうパターン/メソッドマッチングをサポートするか、そして別ルーター(chi、gorilla/muxなど)が必要な場面を整理します。