Go HTTP サービス

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 依存関係のバージョン(3か月後に移行したい)
r := gin.Default()
r.GET("/users", listUsers)               // gin.Context
r.POST("/users", createUser)             // gin.Context
r.GET("/users/:id", getUser)             // gin.Context
// 標準ライブラリに移行したい?すべて handler 署名もすべて変更する必要がある!

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

GO
// 標準ライブラリのバージョン(Go 1.22+,依存関係は一切不要)
パッケージ main

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

type User struct {
    ID   int    `json:"id"`
    Name 文字列 `json:"name"`
}

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

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

    // Go 1.22 ルーティングの強化:メソッド + パスパターン + パス引数
    mux.HandleFunc("GET /users", listUsers)
    mux.HandleFunc("POST /users", createUser)
    mux.HandleFunc("GET /users/{id}", getUser)

    log.Println("サービスの開始は :8080")
    log.Fatal(http.ListenAndServe(":8080", mux))
}

// 基準 handler 署名: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") // パス引数!
    // 検索 ユーザー...
    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("サービスの開始は :8080")
    log.Fatal(http.ListenAndServe(":8080", nil))
}
▶ 試してみよう

テスト:

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

(1) コアの種類

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

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

GO
package main

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

// 方法 1:実装 Handler インターフェース
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:])
}

// 方法 2:HandlerFunc アダプター
func helloHandler(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintf(w, "Hello, %s!", r.URL.Query().Get("name"))
}

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

    // 構造体 Handler
    mux.Handler("/greet", &Greeter{Greeting: "Welcome"})

    // 関数 Handler(HandlerFunc 自動変換)
    mux.HandlerFunc("/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 方法 + パスパターン
    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)

    // ワイルドカードサフィックス:パスプレフィックスのマッチング
    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")
    // 検索 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
    // 更新ロジック...
    w.WriteHeader(http.StatusNoContent)
}

func deleteItem(w http.ResponseWriter, r *http.Request) {
    id := r.PathValue("id")
    _ = id
    // 削除ロジック...
    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)
}
▶ 試してみよう

(1) 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()

    // 正確な経路 > プレフィックスパス
    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/アイテム
アイテム list
$ curl localhost:8080/アイテム/42
item 42
$ curl localhost:8080/アイテム/featured
featured アイテム    # 完全一致が優先される {id} ワイルドカード
🔥 よくある間違い: ルートの登録順序は関係ありません。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,
    }

    // クエリパラメータ
    if r.Method == http.MethodGet {
        resp.Query = r.URL.Query()
    }

    // フォームデータ
    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))
}
▶ 試してみよう

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

GO
package main

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

// JSON レスポンスツール関数
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")
    // シミュレーション検索
    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 クライアント
    participant Mux as ServeMux
    participant Handler as Handler
    
    Client->>Mux: GET /products/1
    Mux->>Mux: ルーティングマッチング
    Mux->>Handler: ServeHTTP(w, r)
    Handler->>Handler: r.PathValue("id") → "1"
    Handler->>Handler: writeJSON(w, 200, product)
    Handler-->>Client: HTTP 200 + JSON body

(2) 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
パッケージ main

import (
    "log"
    "net/http"
)

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

    // 静的ファイルサービス:/static/ 接頭辞 → ./static/ 目次
    mux.Handle("GET /static/", http.StripPrefix("/static/",
        http.FileServer(http.Dir("./static"))))

    // 単一ファイル
    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)
}

// ユーティリティ関数
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 開始日: :8080")
    log.Println("利用可能なエンドポイント:")
    log.Println("  GET    /notes       — すべてのメモを表示する")
    log.Println("  POST   /notes       — ノートを作成する")
    log.Println("  GET    /notes/{id}  — 個別のノートを取得する")
    log.Println("  PUT    /notes/{id}  — 更新履歴")
    log.Println("  DELETE /notes/{id}  — メモを削除する")
    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 サーバー.Shutdown(ctx)os/signal を組み合わせて使用し、終了シグナルを捕捉してください。Shutdown は、すべてのアクティブな接続の処理が完了するまで待機してからシャットダウンします。サーバー.Close() は使用しないでください。これを使用すると、現在処理中のリクエストが強制的に終了されてしまいます。
Q nilhttp.ListenAndServeの2番目の引数として渡すとはどういう意味ですか?
A nilを渡すことは、http.DefaultServeMux(グローバルデフォルトルーター)が使用されることを示します。グローバルルーティングが乱れるのを防ぐため、特にテスト中にルートを分離する場合は、http.NewServeMux()を明示的に作成することをお勧めします。
Q ServeMuxはネストされたサブルートをサポートしていますか?
A 標準ライブラリのServeMuxは、ネストされたルート(サブルートのグループ化)をサポートしていません。共通のプレフィックスを付けて手動でルートを登録するか、chi(非常に軽量なライブラリ)などのサードパーティ製のルーティングライブラリを使用してネストを実装することで、同様の効果を得ることができます。

📖 まとめ


📝 練習問題

  1. 基本演習(難易度 ⭐):簡単なHTTPサービスを作成し、3つのルートを登録してください。GET /timeは現在の時刻をJSON形式で返し、GET /health{"ステータス": "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%