Go: Go HTTP サービス:Handler、ServeMux、Go 1.22 のルーティング機能の強化、JSON

最終更新:2026-08-26

Goの標準ライブラリに含まれるnet/httpパッケージは機能が充実しており、サードパーティ製のフレームワークを使用することなく、本番環境向けのWebサービスを構築することができます。

チームが「Webフレームワークを導入せず、標準ライブラリのみを使用する」と決めた場合、GinやEchoと同じように、わかりやすいルートやミドルウェアを記述できるでしょうか?このレッスンでは、GoのHTTPサービスに関するすべてのコア技術を習得します。

1. 学習内容



2. バックエンドエンジニアの実話

(1) 課題:シンプルなAPI――Ginを採用したが、3か月後、アップグレードの途中で壁にぶつかった

アリスはバックエンドチームのメンバーで、ユーザー管理用のREST APIを設定する必要があります:

「Ginフレームワークを使って、GET /users、POST /users、GET /users/:id の3つのルーティングを記述しました。しかし、3か月後にGo 1.22がリリースされ、標準ライブラリにメソッドパラメータとパスパラメータのネイティブサポートが追加されました。そこでGinへの依存関係を削除したいのですが、そのためにはすべてのハンドラのシグネチャを変更しなければなりません――gin.Context から http.ResponseWriter への変更です。上司は『たった1つの依存関係をなくすためだけに、何百行ものコードをリファクタリングする価値はない』と言いました。」

当時の彼女の決断:

GO
// Gin dependency version (wanted to migrate three months later)
r := gin.Default()
r.GET("/users", listUsers)               // gin.Context
r.POST("/users", createUser)             // gin.Context
r.GET("/users/:id", getUser)             // gin.Context
// Want to migrate to the standard library? All handler signatures must change!

(2) Go 1.22 向けの解決策:標準ライブラリにおけるネイティブルーティング

GO
// Standard library version (Go 1.22+, no dependencies required)
package main

import (
    "encoding/json"
    "log"
    "net/http"
)

type User struct {
    ID   int    `json:"id"`
    Name string `json:"name"`
}

var users = []User{{ID: 1, Name: "Alice"}}

func main() {
    mux := http.NewServeMux()

    // Go 1.22 enhanced routing: method + path pattern + path parameters
    mux.HandleFunc("GET /users", listUsers)
    mux.HandleFunc("POST /users", createUser)
    mux.HandleFunc("GET /users/{id}", getUser)

    log.Println("Server started on :8080")
    log.Fatal(http.ListenAndServe(":8080", mux))
}

// Standard handler signature: http.ResponseWriter + *http.Request
func listUsers(w http.ResponseWriter, r *http.Request) {
    w.Header().Set("Content-Type", "application/json")
    json.NewEncoder(w).Encode(users)
}

func createUser(w http.ResponseWriter, r *http.Request) {
    var u User
    if err := json.NewDecoder(r.Body).Decode(&u); err != nil {
        http.Error(w, err.Error(), http.StatusBadRequest)
        return
    }
    u.ID = len(users) + 1
    users = append(users, u)
    w.WriteHeader(http.StatusCreated)
    json.NewEncoder(w).Encode(u)
}

func getUser(w http.ResponseWriter, r *http.Request) {
    id := r.PathValue("id") // Path parameter!
    // Look up user...
    w.Header().Set("Content-Type", "application/json")
    json.NewEncoder(w).Encode(User{ID: 1, Name: "Alice"})
}

(3) パフォーマンス:Ginスタイル対標準ライブラリ(Go 1.22)

機能 Gin(サードパーティ製) 標準ライブラリ(Go 1.22未満) 標準ライブラリ(Go 1.22以降)
パスパラメータ :id ❌ 手動で解析する必要があります {id}
メソッドルーティング ❌ ハンドラー内で確認 "GET /path"
JSONレスポンス c.JSON() ヘッダーを手動で設定 ヘッダーを手動で設定
依存関係 外部パッケージ 1 つ 0 0
パフォーマンス わずかに遅い(リフレクション) ネイティブ ネイティブ
💡 ヒント: Go 1.22のnet/httpで強化されたルーティング機能は、ほとんどのWebプロジェクトにとって十分です。フレームワーク固有の機能(自動バインディングやバリデーション、充実したミドルウェアエコシステムなど)が必要ない場合は、標準ライブラリを優先してください



3. HTTPの基礎

▶ サンプル:最もシンプルなHTTPサービス

GO
package main

import (
    "fmt"
    "log"
    "net/http"
)

func helloHandler(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintf(w, "Hello, %s!", r.URL.Query().Get("name"))
}

func main() {
    http.HandleFunc("/hello", helloHandler)
    log.Println("Server started on :8080")
    log.Fatal(http.ListenAndServe(":8080", nil))
}
▶ 試してみよう

テスト:

BASH
$ curl "http://localhost:8080/hello?name=Alice"
Hello, Alice!

(2) コアの種類

種類 説明
http.ResponseWriter HTTPレスポンスを記述するためのインターフェース
*http.Request URL、ヘッダー、本文、フォームを含むHTTPリクエスト
http.Handler インターフェース:ServeHTTP(w, r)
http.HandlerFunc 関数アダプタ:通常の関数をハンドラに変換する
http.ServeMux ルートマルチプレクサ

(3) ハンドラ・インターフェースの詳細な説明

GO
package main

import (
    "fmt"
    "log"
    "net/http"
)

// Method 1: Implement the Handler interface
type Greeter struct {
    Greeting string
}

func (g *Greeter) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintf(w, "%s, %s!", g.Greeting, r.URL.Path[1:])
}

// Method 2: HandlerFunc adapter
func helloHandler(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintf(w, "Hello, %s!", r.URL.Query().Get("name"))
}

func main() {
    mux := http.NewServeMux()

    // Struct Handler
    mux.Handle("/greet", &Greeter{Greeting: "Welcome"})

    // Function Handler (HandlerFunc automatic conversion)
    mux.HandleFunc("/hello", helloHandler)

    log.Print(http.ListenAndServe(":8080", mux))
}


4. Go 1.22:ルーティング機能の強化

▶ サンプル:メソッド + パスパターン + パスパラメータ

GO 📖 参照専用
package main

import (
    "encoding/json"
    "log"
    "net/http"
)

type Item struct {
    ID    int     `json:"id"`
    Name  string  `json:"name"`
    Price float64 `json:"price"`
}

var items = []Item{
    {ID: 1, Name: "Laptop", Price: 999.99},
    {ID: 2, Name: "Mouse", Price: 29.99},
}

func main() {
    mux := http.NewServeMux()

    // Go 1.22 method + path pattern
    mux.HandleFunc("GET /items", listItems)
    mux.HandleFunc("POST /items", createItem)
    mux.HandleFunc("GET /items/{id}", getItem)
    mux.HandleFunc("PUT /items/{id}", updateItem)
    mux.HandleFunc("DELETE /items/{id}", deleteItem)

    // Wildcard suffix: path prefix matching
    mux.HandleFunc("GET /items/{path...}", wildcardHandler)

    log.Print(http.ListenAndServe(":8080", mux))
}

func listItems(w http.ResponseWriter, r *http.Request) {
    w.Header().Set("Content-Type", "application/json")
    json.NewEncoder(w).Encode(items)
}

func createItem(w http.ResponseWriter, r *http.Request) {
    var item Item
    if err := json.NewDecoder(r.Body).Decode(&item); err != nil {
        http.Error(w, err.Error(), http.StatusBadRequest)
        return
    }
    item.ID = len(items) + 1
    items = append(items, item)
    w.WriteHeader(http.StatusCreated)
    json.NewEncoder(w).Encode(item)
}

func getItem(w http.ResponseWriter, r *http.Request) {
    id := r.PathValue("id")
    // Look up item...
    _ = id
    w.Header().Set("Content-Type", "application/json")
    json.NewEncoder(w).Encode(items[0])
}

func updateItem(w http.ResponseWriter, r *http.Request) {
    id := r.PathValue("id")
    _ = id
    // Update logic...
    w.WriteHeader(http.StatusNoContent)
}

func deleteItem(w http.ResponseWriter, r *http.Request) {
    id := r.PathValue("id")
    _ = id
    // Delete logic...
    w.WriteHeader(http.StatusNoContent)
}

func wildcardHandler(w http.ResponseWriter, r *http.Request) {
    path := r.PathValue("path")
    w.Header().Set("Content-Type", "text/plain")
    http.Error(w, "Not found: "+path, http.StatusNotFound)
}
論理コード 61 行(40 行制限超過、参照専用)

(2) Go 1.22におけるルーティングモードの比較

モード Go < 1.22 Go 1.22 以降
メソッドマッチング 未対応(ハンドラー内のif文) 対応 "GET /items"
パスパラメータ 未対応 {name} 構文 "GET /items/{id}"
ワイルドカード接尾辞 未対応 {path...} "GET /static/{file...}"
完全なパス /items "GET /items" /items と完全に一致
プレフィックスマッチ /items/ "GET /items/" /items/... に一致

▶ サンプル:ルートの優先順位

GO
package main

import (
    "fmt"
    "log"
    "net/http"
)

func main() {
    mux := http.NewServeMux()

    // Exact path > prefix path
    mux.HandleFunc("GET /items", func(w http.ResponseWriter, r *http.Request) {
        fmt.Fprintln(w, "items list")
    })
    mux.HandleFunc("GET /items/{id}", func(w http.ResponseWriter, r *http.Request) {
        fmt.Fprintf(w, "item %s\n", r.PathValue("id"))
    })
    mux.HandleFunc("GET /items/featured", func(w http.ResponseWriter, r *http.Request) {
        fmt.Fprintln(w, "featured items")
    })

    log.Print(http.ListenAndServe(":8080", mux))
}
▶ 試してみよう

テスト:

BASH
$ curl localhost:8080/items
items list
$ curl localhost:8080/items/42
item 42
$ curl localhost:8080/items/featured
featured items    # Exact match takes priority over {id} wildcard
🔥 よくある間違い: ルートの登録順序は関係ありません。Go 1.22 におけるルートマッチングは、登録順序ではなく 優先順位ルール(完全一致 > プレフィックス > ワイルドカード)に基づいています。たとえ優先順位の低いルートが先に登録されていたとしても、優先順位の高いルートが優先されます



5. リクエストとレスポンス

▶ サンプル:クエリパラメータ、フォーム、JSON

GO 📖 参照専用
package main

import (
    "encoding/json"
    "fmt"
    "log"
    "net/http"
)

type Response struct {
    Method string      `json:"method"`
    Path   string      `json:"path"`
    Query  interface{} `json:"query,omitempty"`
    Form   interface{} `json:"form,omitempty"`
    JSON   interface{} `json:"json,omitempty"`
}

func handler(w http.ResponseWriter, r *http.Request) {
    resp := Response{
        Method: r.Method,
        Path:   r.URL.Path,
    }

    // Query parameters
    if r.Method == http.MethodGet {
        resp.Query = r.URL.Query()
    }

    // Form data
    if r.Method == http.MethodPost {
        contentType := r.Header.Get("Content-Type")
        switch {
        case contentType == "application/x-www-form-urlencoded":
            r.ParseForm()
            resp.Form = r.Form
        case contentType == "application/json":
            var body interface{}
            json.NewDecoder(r.Body).Decode(&body)
            resp.JSON = body
        }
    }

    w.Header().Set("Content-Type", "application/json")
    json.NewEncoder(w).Encode(resp)
}

func main() {
    mux := http.NewServeMux()
    mux.HandleFunc("/", handler)

    log.Print(http.ListenAndServe(":8080", mux))
}
論理コード 42 行(40 行制限超過、参照専用)

▶ サンプル:JSONレスポンス用ユーティリティ関数

GO
package main

import (
    "encoding/json"
    "log"
    "net/http"
)

// JSON response utility function
func writeJSON(w http.ResponseWriter, status int, data interface{}) {
    w.Header().Set("Content-Type", "application/json")
    w.WriteHeader(status)
    json.NewEncoder(w).Encode(data)
}

func writeError(w http.ResponseWriter, status int, message string) {
    writeJSON(w, status, map[string]string{"error": message})
}

type Product struct {
    ID    int     `json:"id"`
    Name  string  `json:"name"`
    Price float64 `json:"price"`
}

func getProduct(w http.ResponseWriter, r *http.Request) {
    id := r.PathValue("id")
    // Simulate lookup
    if id != "1" {
        writeError(w, http.StatusNotFound, "product not found")
        return
    }
    writeJSON(w, http.StatusOK, Product{ID: 1, Name: "Laptop", Price: 999.99})
}

func main() {
    mux := http.NewServeMux()
    mux.HandleFunc("GET /products/{id}", getProduct)

    log.Print(http.ListenAndServe(":8080", mux))
}
▶ 試してみよう
100%
sequenceDiagram
    participant Client as HTTP Client
    participant Mux as ServeMux
    participant Handler as Handler
    
    Client->>Mux: GET /products/1
    Mux->>Mux: Route matching
    Mux->>Handler: ServeHTTP(w, r)
    Handler->>Handler: r.PathValue("id") → "1"
    Handler->>Handler: writeJSON(w, 200, product)
    Handler-->>Client: HTTP 200 + JSON body

(3) HTTPステータスコードのクイックリファレンス

コード 定数 用途
200 http.StatusOK 成功
201 http.StatusCreated リソースの作成に成功しました
204 http.StatusNoContent 成功したが、レスポンス本文がない
400 http.StatusBadRequest クライアントからのリクエストエラー
401 http.StatusUnauthorized アクセス拒否
403 http.StatusForbidden 権限なし
404 http.StatusNotFound リソースが見つかりません
500 http.StatusInternalServerError サーバー内部エラー


6. 静的ファイルサービス

▶ サンプル:静的ファイル

GO
package main

import (
    "log"
    "net/http"
)

func main() {
    mux := http.NewServeMux()

    // Static file service: /static/ prefix → ./static/ directory
    mux.Handle("GET /static/", http.StripPrefix("/static/",
        http.FileServer(http.Dir("./static"))))

    // Single file
    mux.Handle("GET /favicon.ico", http.FileServer(http.Dir("./static")))

    log.Print(http.ListenAndServe(":8080", mux))
}
▶ 試してみよう

7. 完全な例:RESTスタイルのノートAPI

GO
// notes_api.go
package main

import (
    "encoding/json"
    "log"
    "net/http"
    "strconv"
    "sync"
    "time"
)

// ---------- Model ----------

type Note struct {
    ID        int       `json:"id"`
    Title     string    `json:"title"`
    Content   string    `json:"content"`
    CreatedAt time.Time `json:"created_at"`
    UpdatedAt time.Time `json:"updated_at"`
}

// ---------- Store ----------

type NoteStore struct {
    mu     sync.RWMutex
    notes  map[int]Note
    nextID int
}

func NewNoteStore() *NoteStore {
    return &NoteStore{
        notes:  make(map[int]Note),
        nextID: 1,
    }
}

func (s *NoteStore) Create(title, content string) Note {
    s.mu.Lock()
    defer s.mu.Unlock()
    n := Note{
        ID:        s.nextID,
        Title:     title,
        Content:   content,
        CreatedAt: time.Now(),
        UpdatedAt: time.Now(),
    }
    s.nextID++
    s.notes[n.ID] = n
    return n
}

func (s *NoteStore) List() []Note {
    s.mu.RLock()
    defer s.mu.RUnlock()
    result := make([]Note, 0, len(s.notes))
    for _, n := range s.notes {
        result = append(result, n)
    }
    return result
}

func (s *NoteStore) Get(id int) (Note, bool) {
    s.mu.RLock()
    defer s.mu.RUnlock()
    n, ok := s.notes[id]
    return n, ok
}

func (s *NoteStore) Update(id int, title, content string) (Note, bool) {
    s.mu.Lock()
    defer s.mu.Unlock()
    n, ok := s.notes[id]
    if !ok {
        return Note{}, false
    }
    n.Title = title
    n.Content = content
    n.UpdatedAt = time.Now()
    s.notes[id] = n
    return n, true
}

func (s *NoteStore) Delete(id int) bool {
    s.mu.Lock()
    defer s.mu.Unlock()
    _, ok := s.notes[id]
    if !ok {
        return false
    }
    delete(s.notes, id)
    return true
}

// ---------- API ----------

type NotesAPI struct {
    store *NoteStore
}

func NewNotesAPI(store *NoteStore) *NotesAPI {
    return &NotesAPI{store: store}
}

func (api *NotesAPI) Register(mux *http.ServeMux) {
    mux.HandleFunc("GET /notes", api.ListNotes)
    mux.HandleFunc("POST /notes", api.CreateNote)
    mux.HandleFunc("GET /notes/{id}", api.GetNote)
    mux.HandleFunc("PUT /notes/{id}", api.UpdateNote)
    mux.HandleFunc("DELETE /notes/{id}", api.DeleteNote)
}

// Utility functions
func writeJSON(w http.ResponseWriter, status int, data interface{}) {
    w.Header().Set("Content-Type", "application/json")
    w.WriteHeader(status)
    json.NewEncoder(w).Encode(data)
}

func writeError(w http.ResponseWriter, status int, message string) {
    writeJSON(w, status, map[string]string{"error": message})
}

// ---------- Handlers ----------

func (api *NotesAPI) ListNotes(w http.ResponseWriter, r *http.Request) {
    notes := api.store.List()
    writeJSON(w, http.StatusOK, notes)
}

func (api *NotesAPI) CreateNote(w http.ResponseWriter, r *http.Request) {
    var input struct {
        Title   string `json:"title"`
        Content string `json:"content"`
    }
    if err := json.NewDecoder(r.Body).Decode(&input); err != nil {
        writeError(w, http.StatusBadRequest, "invalid JSON body")
        return
    }
    if input.Title == "" {
        writeError(w, http.StatusBadRequest, "title is required")
        return
    }

    note := api.store.Create(input.Title, input.Content)
    writeJSON(w, http.StatusCreated, note)
}

func (api *NotesAPI) GetNote(w http.ResponseWriter, r *http.Request) {
    idStr := r.PathValue("id")
    id, err := strconv.Atoi(idStr)
    if err != nil {
        writeError(w, http.StatusBadRequest, "invalid note ID")
        return
    }

    note, ok := api.store.Get(id)
    if !ok {
        writeError(w, http.StatusNotFound, "note not found")
        return
    }
    writeJSON(w, http.StatusOK, note)
}

func (api *NotesAPI) UpdateNote(w http.ResponseWriter, r *http.Request) {
    idStr := r.PathValue("id")
    id, err := strconv.Atoi(idStr)
    if err != nil {
        writeError(w, http.StatusBadRequest, "invalid note ID")
        return
    }

    var input struct {
        Title   string `json:"title"`
        Content string `json:"content"`
    }
    if err := json.NewDecoder(r.Body).Decode(&input); err != nil {
        writeError(w, http.StatusBadRequest, "invalid JSON body")
        return
    }

    note, ok := api.store.Update(id, input.Title, input.Content)
    if !ok {
        writeError(w, http.StatusNotFound, "note not found")
        return
    }
    writeJSON(w, http.StatusOK, note)
}

func (api *NotesAPI) DeleteNote(w http.ResponseWriter, r *http.Request) {
    idStr := r.PathValue("id")
    id, err := strconv.Atoi(idStr)
    if err != nil {
        writeError(w, http.StatusBadRequest, "invalid note ID")
        return
    }

    if !api.store.Delete(id) {
        writeError(w, http.StatusNotFound, "note not found")
        return
    }
    w.WriteHeader(http.StatusNoContent)
}

func main() {
    store := NewNoteStore()
    api := NewNotesAPI(store)

    mux := http.NewServeMux()
    api.Register(mux)

    log.Println("Note API started on :8080")
    log.Println("Available endpoints:")
    log.Println("  GET    /notes       — List all notes")
    log.Println("  POST   /notes       — Create a note")
    log.Println("  GET    /notes/{id}  — Get a single note")
    log.Println("  PUT    /notes/{id}  — Update a note")
    log.Println("  DELETE /notes/{id}  — Delete a note")
    log.Fatal(http.ListenAndServe(":8080", mux))
}
🔥 よくある間違い: http.Error(w, msg, code) では Content-Type が JSON に設定されません。JSON 形式のエラーを返す場合は、json.NewEncoder(w).Encode(errResp) を使用し、Header() を手動で設定してください。標準ライブラリの http.Error() はプレーンテキストを返します。


❓ よくある質問

Q Go 1.22 のルーティング機能の強化は、Gin と比べてどうですか?
A ほとんどのシナリオでは十分です。Go 1.22はメソッドマッチング、パスパラメータ、ワイルドカードをサポートしています。Ginの付加価値は、リクエストのバインディング/バリデーション、ミドルウェアのエコシステム、およびエラー処理にあります。プロジェクトでこれらの機能が不要な場合は、標準ライブラリの方が軽量で安全です(依存関係がゼロ)。
Q HandlerHandlerFunc の違いは何ですか?
A Handler はインターフェース(ServeHTTP メソッドの実装が必要)であるのに対し、HandlerFunc は関数型アダプタであり、これにより通常の関数が自動的に Handler インターフェースを満たすようになります。この2つは完全に同等であり、mux.Handle("/path", handler)mux.HandleFunc("/path", handlerFunc)は同じ効果を持ちます。
Q パスパラメータを取得するにはどうすればよいですか?
A Go 1.22 以降では、r.PathValue("name") を使用して {name} 型のパスパラメータを取得します。それ以前のバージョンでは、r.URL.Path から手動で解析するか、サードパーティのライブラリを使用する必要があります。パスパラメータの名前は、ルートパターン内の {name} と一致している必要があります。
Q ListenAndServeListenAndServeTLS の違いは何ですか?
A 前者は HTTP(ポート 80)を使用するのに対し、後者は HTTPS(ポート 443)を使用し、証明書と秘密鍵ファイルが必要です。HTTPS の例:http.ListenAndServeTLS(":443", "cert.pem", "key.pem", mux)
Q HTTPサーバーを正常にシャットダウンするにはどうすればよいですか?
A server.Shutdown(ctx)os/signalを組み合わせて使用し、終了シグナルを捕捉してください。Shutdownは、すべてのアクティブな接続の処理が完了するまで待機してから終了します。server.Close()は使用しないでください。これを使用すると、現在処理中のリクエストが強制的に終了されてしまいます。
Q ServeMuxはネストされたサブルートをサポートしていますか?
A 標準ライブラリのServeMuxは、ネストされたルート(サブルートのグループ化)をサポートしていません。共通のプレフィックスを付けて手動でルートを登録するか、chi(非常に軽量なライブラリ)などのサードパーティ製のルーティングライブラリを使用してネストを実装することで、同様の効果を得ることができます。

📖 まとめ


📝 練習問題

  1. 基本演習(難易度 ⭐):簡単なHTTPサービスを作成し、3つのルートを登録してください。GET /timeは現在の時刻をJSON形式で返し、GET /health{"status": "ok"}を返し、GET /versionはバージョン番号を返します。Go 1.22の拡張ルーティング構文を使用してください。

  2. 上級問題(難易度 ⭐⭐)ToDoリストAPIを実装してください。要件:(1) CRUD操作をすべて実装すること;(2) Go 1.22のメソッドおよびパスベースのルーティングを使用すること;(3) リクエストとレスポンスはJSON形式とすること;(4) メモリ内ストレージ(map + RWMutexによる保護)を使用すること; (5) 適切なHTTPステータスコードを返すこと。

  3. 課題(難易度:⭐⭐⭐)URL短縮サービスを実装してください。要件:(1) POST /shortenは長いURLを受け取り、短いコード(6文字のランダムな文字列)を返すこと;(2) GET /{code}は元のURLへの301リダイレクトを実行すること; (3) アクセス統計:GET /stats/{code}は訪問回数を返すこと;(4) -raceを使用して並行処理の安全性を検証すること;(5) RWMutexを使用して統計カウンターを保護すること。

Web-Tutorial.com

Web-Tutorial 技術チーム

複数の開発者によって共同維持されているプログラミングチュートリアルプラットフォーム。各チュートリアルは専門分野の開発者が執筆・レビューしています。正確で信頼性の高いコンテンツを目指しています — 問題を見つけた場合はお知らせください。

100%