2-1. net/http - 標準ライブラリで本番運用できるHTTPサーバー

所要時間: 40-60分(がっつりなら2セッション分) コミット内容: ~/learn/go/level2/day01/ に最小HTTPサーバーと本番用 http.Server 設定例をコミット


このレッスンのゴール

  • http.Handler インターフェース(ServeHTTP(w, r))を自分で実装できる
  • 関数を http.HandlerFunc 経由で Handler に変換できる
  • ResponseWriter の Header/Write/WriteHeader の順序を守れる
  • 本番用 http.Server のタイムアウト4種類を設定できる
  • Slowloris 対策と MaxBytesReader が打てる

なぜ学ぶか - Level 2 の出発点として

Express の代わりに gin? echo? それとも標準?」 - Go 1.22 以降は 標準 net/http だけで本番運用が現実的になった。Docker、Kubernetes、Prometheus も net/http ベース。ここを飛ばしてフレームワークに行くと、トラブルシューティングで内部が辿れなくなる。Level 2 全体(ルーティング、JSON、ミドルウェア、エラー処理)が net/http のインターフェース設計を理解している前提で進む。

前章とのつながり

1-6_構造体とメソッド で扱った 「インターフェースは1メソッドの契約」 の感覚が、ここで http.Handler という形で具現化する。さらに 1-7_CLIアプリ で書いた「サブコマンド分岐」の設計が、本章では「メソッド + パスごとのハンドラ分岐」に発展する。

これができると何が嬉しいか

  • go run main.go で本番に近いサーバー が立つ - フレームワーク不要
  • OSS の Go サーバー実装が読める - Kubernetes / Docker のソースに突撃できる
  • Slowloris で落ちないサーバー が書ける - セキュリティの基本線

ストーリー導入: net/http は「インターフェース1枚で組み立てるレゴ」

http.HandlerServeHTTP(w, r) という1メソッドだけの契約。これを実装すれば、関数でも構造体でも何でもハンドラになれる。ミドルウェアは「ハンドラを受け取って、別のハンドラを返す関数」。レゴブロックのように 小さな部品を組み合わせるだけで、ログ・認証・タイムアウトを後付け できる。これが Go の「シンプルな抽象が大きな表現力を生む」哲学の代表例。


大前提: なぜ Go の net/http を学ぶか

Node.js + Express、Python + Flask、Ruby + Rails のように、ほとんどの言語は「Webフレームワーク」を入れて初めて本番サーバーが立つ。Go は違う。標準ライブラリ net/http だけで、Kubernetes・Docker・Terraform・Prometheus といった世界的に動いているOSSの大半がHTTPを喋っている。

Kubernetes の API サーバーも、Docker デーモンも、Prometheus も net/http ベース。Web フレームワークは使っていない。

つまり、net/http を理解する = 本番OSSの「読み解ける目」を手に入れるということ。これを飛ばしてフレームワーク(gin / echo)に行くと、トラブった時に内部を辿れなくなる。

FE出身(TypeScript)の人へ: Express の app.get('/', (req, res) => {}) に相当する処理を、Go は「言語に内蔵された関数」として持っている。require('express') が要らない世界。


セッション①: net/http の設計思想と基本(30分)

0. 録画スタート&作業ディレクトリ

mkdir -p ~/log ~/learn/go/level2/day01
cd ~/learn/go/level2/day01
script ~/log/go_level2_day01.log
 
# Go モジュール初期化
go mod init example.com/level2/day01

1. まずは動かす - 最小HTTPサーバー

// main.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 main.go
# 別ターミナルで
curl -i http://localhost:8080/

このコードで何が起きているか

たった8行で、本番でも(少し手を加えれば)使えるHTTPサーバーが立つ。Node.js なら expressnpm install する所だが、Go は標準ライブラリだけ。

内部の流れ:

  1. http.HandleFunc("/", fn) で「/ というパスに来た時の処理」をデフォルトの ServeMux に登録
  2. http.ListenAndServe(":8080", nil) で 8080 ポートで listen を開始。第2引数 nil は「デフォルトの ServeMux を使う」の意味
  3. リクエストが来るたび、Go ランタイムは goroutine を1本立てて ハンドラを実行する

Node + Express との対比

Express の同等コード:

import express from 'express';
const app = express();
app.get('/', (req, res) => res.send('Hello'));
app.listen(8080);

表面的には似ているが、内部モデルが根本的に違う:

観点Node.js / ExpressGo / net/http
並行モデルシングルスレッド + イベントループリクエストごとに goroutine(数千同時OK)
ブロッキング処理NG(イベントループ詰まる)OK(goroutine 単位で詰まるだけ)
CPU バウンド処理worker_threads が必要普通に書ける
フレームワーク必須度ほぼ必須標準で本番運用可能

FE出身者が最初にハマるのは「time.Sleep(5*time.Second) を書いても他のリクエストを止めない」という点。Node の感覚だと「サーバー全体が止まる」と思いがちだが、Go は止まらない。

2. http.Handler インターフェースが全ての中心

// http.Handler は標準ライブラリ内で以下のように定義されている
type Handler interface {
	ServeHTTP(w ResponseWriter, r *Request)
}

http.Handler とは「HTTPリクエストに応答できる何か」の最小契約

このインターフェースを満たすあらゆる型がHTTPハンドラになる。これが Go HTTP 設計の核心。

たった1メソッド ServeHTTP(w, r) を実装すれば、その型はサーバーに登録できる。クラス継承や extends Controller のような重い構造はない。

TypeScript で言うと:

interface Handler {
  serveHTTP(w: ResponseWriter, r: Request): void;
}

Go では implements キーワードがなく、メソッドを実装したら自動でインターフェースを満たす(structural typing)。

// 自前で Handler インターフェースを満たす型を作る
package main
 
import (
	"fmt"
	"net/http"
)
 
type GreetingHandler struct {
	Greeting string
}
 
// このメソッドを持っているだけで、GreetingHandler は http.Handler になる
func (h *GreetingHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	fmt.Fprintf(w, "%s, %s\n", h.Greeting, r.URL.Path[1:])
}
 
func main() {
	mux := http.NewServeMux()
	mux.Handle("/", &GreetingHandler{Greeting: "Hello"})
	mux.Handle("/bonjour/", &GreetingHandler{Greeting: "Bonjour"})
 
	http.ListenAndServe(":8080", mux)
}

設計思想: なぜ「インターフェース1つ」で済むのか

Go の格言「Accept interfaces, return structs」。標準ライブラリは小さなインターフェースを受け取って、巨大な機能を組み立てる。

http.Handler は1メソッド。io.Reader も1メソッド。io.Writer も1メソッド。1メソッドの小さなインターフェースを組み合わせるのが Go 流。

これにより、ハンドラの差し替え・テスト・ミドルウェアでのラップが極めて簡単になる。後の 2-4 ミドルウェア でこの恩恵を体感する。

3. http.HandlerFunc アダプタ - 関数を Handler に変換

// 標準ライブラリ内の定義
type HandlerFunc func(ResponseWriter, *Request)
 
// HandlerFunc 自身に ServeHTTP メソッドが生えていて、それは「自分自身を呼ぶ」だけ
func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) {
	f(w, r)
}

HandlerFunc は「関数を Handler 扱いするための糊」

普通の関数 func(w, r)http.Handler インターフェースに 変換 するアダプタ。

関数に ServeHTTP メソッドを生やすという「型の上にメソッドを定義」できる Go ならではの仕掛け。これにより、関数型もインターフェースを満たせる

handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
	fmt.Fprintln(w, "ok")
})
mux.Handle("/health", handler)

よく使う http.HandleFunc(path, fn) は、内部で mux.Handle(path, http.HandlerFunc(fn)) を呼んでいるだけ。便利関数

設計思想: なぜクラス継承が要らないか

Express の app.get, app.use は内部で配列にコールバックを積んでいる。Rails の ApplicationController は継承を強制する。

Go はそのどちらでもなく、「ServeHTTP という1メソッドを持つ何か」を受け入れる。これにより:

  • 関数だけでも書ける(軽量)
  • 構造体に状態を持たせたハンドラも書ける(DB コネクション・ロガーを持たせる時に有用)
  • 既存型を後付けで Handler 化できる
  • テストではモック構造体を渡すだけ

シンプルな抽象が広い表現力を生む典型例。io.Reader io.Writer error も同じ思想で作られている。

4. ResponseWriter と *Request の本質

func handler(w http.ResponseWriter, r *http.Request) {
	// リクエスト情報
	method := r.Method        // "GET", "POST", ...
	path := r.URL.Path        // "/users/42"
	query := r.URL.Query()    // url.Values 型のマップ
	host := r.Host            // "api.example.com"
	ua := r.UserAgent()       // User-Agent ヘッダ
	body := r.Body            // io.ReadCloser(後で必ず Close)
	_ = method; _ = path; _ = query; _ = host; _ = ua; _ = body
 
	// レスポンス書き込み
	w.Header().Set("Content-Type", "application/json")
	w.WriteHeader(http.StatusOK)
	w.Write([]byte(`{"ok": true}`))
}

ResponseWriter は「インターフェース」、Request は「構造体ポインタ」

なぜ片方はインターフェース、片方はポインタなのか。これは設計上の理由がある。

  • ResponseWriter (interface): レスポンス生成は 戦略を差し替えたい ことが多い(圧縮、バッファリング、ロギング、HTTP/2 対応)。インターフェースなら自由にラップできる
  • Request (struct pointer): リクエストは「来たデータ」であり、ハンドラ間で 同一のものを参照 したい。コピーされたら困る(Body のストリームが分裂する)。ポインタが自然

ResponseWriter の3メソッド(だけ)

type ResponseWriter interface {
    Header() Header                  // レスポンスヘッダのマップを返す
    Write([]byte) (int, error)       // ボディに書く
    WriteHeader(statusCode int)      // ステータスコードを送る
}

たった3つ。これだけで全 HTTP レスポンスが組める。

落とし穴: WriteHeader の順序

w.WriteHeader() を呼んだ後にヘッダを変更してもクライアントには届かない

// NG: ヘッダ送信後に Content-Type を設定しても遅い
w.WriteHeader(http.StatusOK)
w.Header().Set("Content-Type", "application/json")  // 無視される
w.Write([]byte(`{"ok":true}`))

正しい順序: ヘッダ設定 → WriteHeader → Write

// OK
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
w.Write([]byte(`{"ok":true}`))

アンチパターン: WriteHeader を2回呼ぶ

w.WriteHeader(http.StatusOK)
// ... 何か処理 ...
if err != nil {
    w.WriteHeader(http.StatusInternalServerError)  // 警告: superfluous WriteHeader call
}

なぜNG: HTTPプロトコル上、ステータスコードは1リクエストにつき1回しか送れない。2回目は無視され、net/http がログに警告を吐く。早期 return か、フラグで状態管理する

r.Body は必ず Close する(POST/PUT 時)

defer r.Body.Close()
body, err := io.ReadAll(r.Body)

Body は io.ReadCloser。閉じないと コネクションが Keep-Alive 再利用されない → ファイルディスクリプタとメモリリーク。HTTP サーバーが defer してくれる場合もあるが、自分でも defer する癖を付ける。


セッション②: 本番運用 - タイムアウトとセキュリティ(30分)

5. デフォルト http.ListenAndServe は「本番では使うな」

// よく見るコード
http.ListenAndServe(":8080", nil)

http.ListenAndServe(":8080", nil) は本番禁止

このコードには タイムアウトがゼロ秒も設定されていない。つまり:

  • 悪意あるクライアントが1バイトずつ送り続けるとコネクションが永遠に開いたまま
  • 体感では「サーバーが応答しない」状態に陥る
  • ファイルディスクリプタを食い尽くす → サーバー全体が新規接続を受け付けなくなる

これが Slowloris 攻撃 の入口。2009年に有名になり、今でも素のHTTPサーバーへの基本的な攻撃手法として現役。

6. 本番用 http.Server の設定

package main
 
import (
	"context"
	"log/slog"
	"net"
	"net/http"
	"os"
	"time"
)
 
func main() {
	mux := http.NewServeMux()
	mux.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) {
		w.Write([]byte("ok"))
	})
 
	srv := &http.Server{
		Addr:    ":8080",
		Handler: mux,
 
		// 接続してから「リクエストヘッダ全て」を送り終わるまでの上限
		ReadHeaderTimeout: 5 * time.Second,
 
		// リクエスト本体(ボディ含む)全体を読み終わるまでの上限
		ReadTimeout: 30 * time.Second,
 
		// レスポンス書き込みの上限(ハンドラの処理時間込み)
		WriteTimeout: 30 * time.Second,
 
		// Keep-Alive で接続を維持するアイドル時間の上限
		IdleTimeout: 120 * time.Second,
 
		// リクエストヘッダの最大サイズ(デフォルト 1MB)
		MaxHeaderBytes: 1 << 20,
 
		// ベースとなる context
		BaseContext: func(_ net.Listener) context.Context {
			return context.Background()
		},
	}
 
	logger := slog.New(slog.NewJSONHandler(os.Stdout, nil))
	logger.Info("starting server", "addr", srv.Addr)
 
	if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
		logger.Error("server error", "err", err)
		os.Exit(1)
	}
}

各タイムアウトの役割

タイムアウト何の上限か推奨値の感覚
ReadHeaderTimeoutヘッダ送信完了まで5-10秒(Slowloris 対策の主役)
ReadTimeoutボディ送信完了まで30秒-1分(大ファイルアップロードなら長め)
WriteTimeoutレスポンス送信完了まで30秒-1分(ハンドラ実行時間 + 送信)
IdleTimeoutKeep-Alive で次のリクエスト待ち2分前後(LB の Idle と合わせる)

本番では全部設定する。デフォルトはゼロ = 無制限 = 危険。

ReadHeaderTimeout が最も重要な理由

Slowloris 攻撃は「ヘッダを1バイトずつ送って接続を保持」する。ReadHeaderTimeout を5秒に設定すれば、ヘッダ全体を5秒以内に送れない接続は 即切断

AWS ALB / GCP LB の前段でカバーされることも多いが、自分のサーバーでも防御線を張るのが正解(Defense in Depth)。

7. 内部の本質: goroutine per request

クライアントA ──┐
クライアントB ──┼─→ Listener ─→ Accept() ループ
クライアントC ──┘                  │
                                   ├─ go func() { ハンドラA }  ← goroutine 1
                                   ├─ go func() { ハンドラB }  ← goroutine 2
                                   └─ go func() { ハンドラC }  ← goroutine 3

1リクエスト = 1 goroutine

Go サーバーは すべてのリクエストを独立した goroutine で処理する。これが Node.js との根本的な違い。

結果:

  • 同期コードがそのまま書ける(async/await 不要)
  • 一つのリクエストでブロッキングしても他に影響なし
  • 数千〜数万の同時接続を1プロセスで捌ける

goroutine は OS スレッドより遥かに軽い(初期スタック 2KB)。数万本立てても OS スレッドは数十本で済む(M:N スケジューラ)。

アンチパターン: ハンドラ内でグローバル変数を直接書き換える

var counter int  // グローバル
 
func handler(w http.ResponseWriter, r *http.Request) {
    counter++  // 競合状態!
    fmt.Fprintln(w, counter)
}

なぜNG: 同時に1000リクエストが来たら、counter++複数 goroutine から同時実行される。これは race condition(競合状態)で、go run -race で実行するとすぐ検出される。

修正: sync/atomicatomic.AddInt64 か、sync.Mutex で保護。これは Level 3 並行処理で深掘る。

8. 実装: タイムアウト付きエコーサーバー

// echo_server.go
package main
 
import (
	"context"
	"io"
	"log/slog"
	"net"
	"net/http"
	"os"
	"time"
)
 
func main() {
	logger := slog.New(slog.NewJSONHandler(os.Stdout, nil))
 
	mux := http.NewServeMux()
 
	mux.HandleFunc("/echo", func(w http.ResponseWriter, r *http.Request) {
		if r.Method != http.MethodPost {
			http.Error(w, "POST only", http.StatusMethodNotAllowed)
			return
		}
 
		// ボディサイズ制限(DoS 対策)
		r.Body = http.MaxBytesReader(w, r.Body, 1<<20) // 1MB
		defer r.Body.Close()
 
		body, err := io.ReadAll(r.Body)
		if err != nil {
			http.Error(w, "body too large or read error", http.StatusBadRequest)
			return
		}
 
		w.Header().Set("Content-Type", "text/plain; charset=utf-8")
		w.WriteHeader(http.StatusOK)
		w.Write(body)
	})
 
	mux.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) {
		w.WriteHeader(http.StatusOK)
		w.Write([]byte("ok"))
	})
 
	srv := &http.Server{
		Addr:              ":8080",
		Handler:           mux,
		ReadHeaderTimeout: 5 * time.Second,
		ReadTimeout:       30 * time.Second,
		WriteTimeout:      30 * time.Second,
		IdleTimeout:       120 * time.Second,
		MaxHeaderBytes:    1 << 20,
		BaseContext: func(_ net.Listener) context.Context {
			return context.Background()
		},
	}
 
	logger.Info("starting server", "addr", srv.Addr)
	if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
		logger.Error("server failed", "err", err)
		os.Exit(1)
	}
}
# 動作確認
go run echo_server.go &
 
curl -X POST -d "hello" http://localhost:8080/echo
# → hello
 
# 1MB 超のリクエストはエラー
dd if=/dev/zero bs=1M count=2 2>/dev/null | curl -X POST --data-binary @- http://localhost:8080/echo
# → "body too large or read error" (400)
 
curl http://localhost:8080/health
# → ok

http.MaxBytesReader が必須な理由

Body 読み込み中に メモリを食い尽くす DoS 攻撃 がある。攻撃者が Content-Length を偽って巨大データを送ると、io.ReadAll(r.Body) が OOM する。

http.MaxBytesReader(w, body, 1<<20)読める上限を1MBに制限。超えると自動的にエラーを返し、コネクションも閉じる。

鉄則: 全てのPOST/PUTエンドポイントに MaxBytesReader

落とし穴: タイムアウトと SSE / ロングポーリング

WriteTimeout を30秒に設定すると、Server-Sent Events (SSE) や WebSocket、ロングポーリングが30秒で切断される。

対策:

  • SSE 専用のエンドポイントは別 http.Server で運用する
  • もしくは WriteTimeout を0(無制限)にして、ハンドラ内で context.WithTimeout で個別管理
  • WebSocket は http.Hijacker で TCP 接続を奪うので、HTTP タイムアウトの管轄外

9. セキュリティチェックリスト(本番投入前)

本番投入前の最低ライン

  • ReadHeaderTimeout 設定済み(Slowloris 対策)
  • ReadTimeout / WriteTimeout 設定済み
  • MaxBytesReader でボディ上限あり
  • エラーメッセージにスタックトレース・SQL 文・ファイルパスを含めない
  • http.Error でクライアントに返す文字列を吟味(内部情報を漏らさない)
  • TLS 必須: 平文 HTTP は本番禁止(http.Server.ListenAndServeTLS
  • レート制限(ミドルウェアで)
  • Graceful shutdown(Level 3 で扱う)

これを守らないだけで、CTF 初心者でも落とせるサーバーになる。


練習課題

  1. /time エンドポイントを作り、JSON で現在時刻を返す(time.Now().Format(time.RFC3339)
  2. /slow エンドポイントで time.Sleep(10 * time.Second) し、別ターミナルから curl を同時に投げて「他のリクエストが詰まらない」ことを確認
  3. WriteTimeout を3秒に設定して /slow を呼ぶ → タイムアウトでクライアント側がエラーを受け取ることを確認
  4. 自前 struct で http.Handler を実装し、コンストラクタで *slog.Logger を受け取って ServeHTTP でロギング
  5. http.MaxBytesReader を外して /echo を実装し、dd if=/dev/zero bs=1M count=100 | curl ... で挙動を観察(メモリ消費に注意、すぐ止める)

締め: git で証跡を残す

cd ~/learn/go/level2/day01
git init
git add .
git commit -m "feat(go-http): 標準ライブラリで本番タイムアウト付きHTTPサーバー実装"
exit  # script から抜ける

アンチパターン集 - やらかし事例

net/http の定番事故

1. http.ListenAndServe(":8080", nil) を本番で使う タイムアウトゼロ秒 = Slowloris 直撃ライン。CTF 初心者でも落とせる。

2. WriteHeader を複数回呼ぶ

w.WriteHeader(200)
if err != nil { w.WriteHeader(500) }  // superfluous warning

HTTP 上はステータスコード1回限定。早期 return か フラグ管理

3. r.Body.Close() 忘れ Keep-Alive で接続再利用されず、FD 漏れ。defer r.Body.Close() を癖に。

4. ハンドラ内グローバル変数の競合書き込み

var counter int
func h(w, r) { counter++ }  // race condition

go run -race で必ず検出。sync/atomicsync.Mutex

5. エラーレスポンスに内部情報を含める

http.Error(w, err.Error(), 500)  // SQL 文・パスがクライアントへ漏洩

ユーザー向けは「サーバーエラー」、内部はログにだけ。

対比表で違いを明確化

http.Handler vs http.HandlerFunc

観点http.Handlerhttp.HandlerFunc
インターフェース関数型
実装構造体に ServeHTTP メソッド関数そのもの
状態フィールドで持てる(DB/ロガー)クロージャでキャプチャ
用途状態付きハンドラ軽量ハンドラ

Node.js (Express) vs Go (net/http)

観点Expressnet/http
並行モデルイベントループ1リクエスト1 goroutine
ブロッキングNG(全体停止)OK(goroutine単位)
フレームワーク必須不要
タイムアウトデフォルト無し明示設定(4種)

自己評価チェックリスト

手を動かせた

  • 最小 HTTP サーバーを go run で起動
  • 構造体ハンドラで ServeHTTP を実装した
  • http.HandlerFunc で関数をハンドラ化した
  • http.Server で4タイムアウトを設定
  • MaxBytesReader で巨大ボディを弾いた

説明できる

  • http.Handler インターフェースの定義を空で書ける
  • WriteHeader 後にヘッダ変更が無効になる理由を1分で
  • ReadHeaderTimeout と Slowloris の関係
  • 1リクエスト1 goroutine モデルが Node と違う理由

やらかし回避

  • nil ハンドラ + デフォルト ListenAndServe を本番禁止と言える
  • r.Body.Close() を defer で書く習慣
  • エラーメッセージで内部情報を漏らさない

詰まった時のチートシート

やりたいことコード
最小HTTPサーバーhttp.ListenAndServe(":8080", nil)(本番は別物)
ハンドラ登録mux.HandleFunc("/path", fn)
ステータス送るw.WriteHeader(http.StatusXxx)
ヘッダ設定w.Header().Set("X-Foo", "bar")(WriteHeader 前に)
JSON 返すw.Header().Set("Content-Type", "application/json")json.NewEncoder(w).Encode(obj)
Body 読むdefer r.Body.Close()io.ReadAll(r.Body)
Body サイズ制限r.Body = http.MaxBytesReader(w, r.Body, 1<<20)
メソッド分岐if r.Method != http.MethodPost { ... }(Go 1.22+ は ServeMux で書ける)

「実務OK」基準

  • http.Handler で構造体ハンドラが書ける: DB / Logger を持たせた状態付きハンドラ
  • タイムアウトを4種類すべて設定した http.Server を即書ける
  • Slowloris と ReadHeaderTimeout の関係を後輩に説明できる
  • r.Body.Close()http.MaxBytesReader を反射的に書く
  • 「素の net/http で十分」と言えるだけの自信を持つ

さらに深掘りするなら

  • 標準ライブラリのソース: src/net/http/server.go - ServeHTTP の実装を読むと開眼する
  • 公式 blog: The Go Blog - net/http 関連記事
  • 書籍: 『Go 言語による並行処理』(Katherine Cox-Buday) - HTTP サーバーが goroutine をどう使うかの背景
  • OSS 実例: Kubernetes の staging/src/k8s.io/apiserver/pkg/server/ - net/http 上に API Server を構築している好例
  • Cloudflare ブログ: “The complete guide to Go net/http timeouts” - タイムアウト解説の決定版

次のレッスン

2-2 ルーティング で ServeMux の使い倒し方と、chi / gin / echo の比較に進む。Go 1.22 で大幅強化されたルーティング機能(メソッド指定、パスパラメータ)を本気で学ぶ。

つながりの予告

  • 本章の mux.HandleFunc("/", h) は次章で mux.HandleFunc("GET /todos/{id}", h) に発展
  • http.Handler インターフェースは 2-4_ミドルウェア で「ハンドラを返す関数」として核心になる
  • タイムアウト・MaxBytesReader は 3-7_本番準備 のチェックリストに再登場