Go実践 #1 はじめてのHTTPサーバー

読了 5分

Go実践シリーズのスタートです。これまでの基礎・中級・上級で見てきた道具たちが — 実際のバックエンドを書くときどう組み立てられるかを、6編にわたって整理します。

  1. はじめてのHTTPサーバー (この記事) — net/http、ListenAndServe、graceful shutdown
  2. ルーティング — Go 1.22+ ServeMuxのパターンマッチング
  3. JSON入出力と入力検証
  4. DB連携database/sql、トランザクション
  5. ミドルウェアパターン
  6. テストとデプロイhttptest、Docker

Goがバックエンドで強い最大の理由 — 標準のnet/httpだけでプロダクションサーバーが可能、という点です。この記事はその出発点です。

最小のサーバー #

hello.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)
}
実行
go run hello.go
curl http://localhost:8080
# Hello, world!

19行(空行を除けば7行)。外部依存ゼロ。Express、FastAPIのようなフレームワークの助けなしに — 標準ライブラリだけで。

3つのコア型 #

net/httpのコア
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 #

2つの登録方法
// 関数として登録
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().SetWriteHeaderまたは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 2

Query()が — 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自体がメソッドマッチングをサポートします。次の記事で詳しく。

リクエストボディの読み込み #

POSTボディ
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で詳しく。

ボディサイズの制限 #

MaxBytesReader
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構造体で設定を明示 #

明示的な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を送ったとき — 進行中のリクエストを終わらせて終了したい場合。

graceful shutdown
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")
}

流れ:

  1. 別ゴルーチンでListenAndServe — メインはシグナル待機
  2. signal.NotifyContext — Go 1.16+の標準パターン。コンテキストでシグナル処理
  3. 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アダプタ #

関数をHandlerに
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型 — HandlerResponseWriterRequest
  • HandleFuncが一般的、状態が必要なら構造体 + ServeHTTP
  • レスポンス作成の順序 — Header → WriteHeader → Write
  • http.Serverを直接作ってタイムアウトは常に設定
  • graceful shutdownsignal.NotifyContext + srv.Shutdown(ctx)
  • ハンドラは並行実行 — 共有状態は同期化

次の記事(#2 ルーティング)では — Go 1.22+のServeMuxがどうパターン/メソッドマッチングをサポートするか、そして別ルーター(chi、gorilla/muxなど)が必要な場面を整理します。

X