HTTPプログラミング

レッスン22:HTTPプログラミング

🎯 生活での例え

あなたがレストランを経営していると想像してください:

Goのnet/httpパッケージは「レストラン管理システム」のようなもので、高性能で安定したWebサービスを素早く構築するのに役立ちます。


📚 コアコンセプト

コンセプト 説明
http.Get/Post HTTPリクエストを送信してリモートリソースを取得
http.ListenAndServe HTTPサーバーを起動してポートをリッスン
Handlerインターフェース リクエスト処理基準を定義するコアインターフェース
HandlerFunc 通常の関数をHandlerに変換するアダプター
ServeMux HTTPリクエストマルチプレクサー(ルーター)
ミドルウェア リクエスト処理の前後に共通ロジックを挿入するパターン

📝 基本構文と使い方

1. HTTPリクエストの送信

GO
package main

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

func main() {
	// GETリクエストを送信
	resp, err := http.Get("https://httpbin.org/get")
	if err != nil {
		fmt.Println("リクエスト失敗:", err)
		return
	}
	defer resp.Body.Close() // 💡 レスポンスボディは必ず閉じる(リソースリーク防止)

	// レスポンス内容を読み取る
	body, err := io.ReadAll(resp.Body)
	if err != nil {
		fmt.Println("読み取り失敗:", err)
		return
	}

	fmt.Println("ステータスコード:", resp.StatusCode)
	fmt.Println("レスポンス:", string(body))
}
💡 ヒント: resp.Bodyは使用後に必ずClose()で閉じる必要があります。さもないと接続リークが発生します。deferを使用するのがベストプラクティスです。

2. POSTリクエストの送信

GO
package main

import (
	"bytes"
	"encoding/json"
	"fmt"
	"io"
	"net/http"
)

func main() {
	// JSONデータを構築
	data := map[string]string{
		"username": "gopher",
		"email":    "gopher@example.com",
	}
	jsonData, _ := json.Marshal(data)

	// POSTリクエストを送信
	// 💡 第3パラメータはリクエストボディ、io.Reader型が必要
	resp, err := http.Post(
		"https://httpbin.org/post",
		"application/json",
		bytes.NewBuffer(jsonData),
	)
	if err != nil {
		fmt.Println("リクエスト失敗:", err)
		return
	}
	defer resp.Body.Close()

	body, _ := io.ReadAll(resp.Body)
	fmt.Println("ステータスコード:", resp.StatusCode)
	fmt.Println("レスポンス:", string(body))
}
💡 ヒント: http.PostのContent-Typeパラメータは非常に重要です。サーバーはそれに基づいてリクエストボディの形式を解析します。

3. HTTPサーバーの起動

GO
package main

import (
	"fmt"
	"net/http"
)

func main() {
	// ルートハンドラーを登録
	http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
		fmt.Fprintf(w, "ホームページへようこそ!")
	})

	http.HandleFunc("/hello", func(w http.ResponseWriter, r *http.Request) {
		fmt.Fprintf(w, "こんにちは、Gopher!")
	})

	// サーバーを起動、ポート8080をリッスン
	// 💡 ListenAndServeはサーバーがシャットダウンするまでブロック
	fmt.Println("サーバーが http://localhost:8080 で起動しました")
	err := http.ListenAndServe(":8080", nil)
	if err != nil {
		fmt.Println("サーバー起動失敗:", err)
	}
}
💡 ヒント: 第2パラメータにnilを渡すと、デフォルトのDefaultServeMuxが使用されます。本番環境ではカスタムルーターの作成を推奨します。

4. Handlerインターフェース

GO
// Handlerインターフェースの定義:ServeHTTPメソッドを実装するすべての型がHandler
type Handler interface {
	ServeHTTP(ResponseWriter, *Request)
}
GO
package main

import (
	"fmt"
	"net/http"
)

// カスタムHandler型
type GreetingHandler struct {
	Message string
}

// HandlerインターフェースのServeHTTPメソッドを実装
func (g GreetingHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	fmt.Fprintf(w, g.Message)
}

func main() {
	// カスタムHandlerを使用
	handler := GreetingHandler{Message: "こんにちは、これはカスタムHandlerです!"}
	http.Handle("/greet", handler) // 💡 注意:Handleを使用、HandleFuncではない

	http.ListenAndServe(":8080", nil)
}
💡 ヒント: HandleHandlerインターフェースを受け取り、HandleFuncは関数を受け取ります。機能的には等価で、形式が異なるだけです。

5. HandlerFuncアダプター

GO
package main

import (
	"fmt"
	"net/http"
)

func myHandler(w http.ResponseWriter, r *http.Request) {
	fmt.Fprintf(w, "HandlerFuncでアダプトされたHandler")
}

func main() {
	// HandlerFuncは通常の関数をHandlerに変換
	// 💡 本质上は型変換:type HandlerFunc func(ResponseWriter, *Request)
	http.Handle("/adapted", http.HandlerFunc(myHandler))

	// 等価な形式(より一般的に使用)
	http.HandleFunc("/simple", func(w http.ResponseWriter, r *http.Request) {
		fmt.Fprintf(w, "HandleFuncを直接使うとより簡潔")
	})

	http.ListenAndServe(":8080", nil)
}
💡 ヒント: HandlerFuncは型アダプターで、通常の関数がHandlerインターフェースを満たすことを可能にします。


🧪 実践例題

例:シンプルな静的ファイルサーバー(難易度⭐)

GO
package main

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

func main() {
	// デフォルトルーターを使用して静的ファイルサービスを登録
	// StripPrefixはURLから"/static/"プレフィックスを削除
	// FileServerはディレクトリ内のファイルへのアクセスを提供
	fs := http.FileServer(http.Dir("./public"))
	http.Handle("/static/", http.StripPrefix("/static/", fs))

	// ホームページハンドラー
	http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
		if r.URL.Path != "/" {
			http.NotFound(w, r) // 💡 404処理
			return
		}
		fmt.Fprintf(w, "ウェブサイトへようこそ!")
	})

	fmt.Println("サーバーが http://localhost:8080 で実行中")
	log.Fatal(http.ListenAndServe(":8080", nil))
}
▶ 試してみよう

実行:

BASH
# テストディレクトリとファイルを作成
mkdir -p public
echo "<h1>Hello</h1>" > public/index.html

# サーバーを実行
go run main.go

# テストアクセス(別のターミナル)
curl http://localhost:8080/
curl http://localhost:8080/static/index.html

例:カスタムルーティングとミドルウェア(難易度⭐⭐)

GO
package main

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

// ログミドルウェア:各リクエストの処理時間を記録
func LoggingMiddleware(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		start := time.Now()
		log.Printf("開始 %s %s", r.Method, r.URL.Path)

		next.ServeHTTP(w, r) // 💡 次のハンドラーを呼び出す

		log.Printf("完了 %s %s 所要時間 %v", r.Method, r.URL.Path, time.Since(start))
	})
}

// 認証ミドルウェア:リクエストヘッダーのTokenをチェック
func AuthMiddleware(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		token := r.Header.Get("Authorization")
		if token == "" {
			http.Error(w, "認証されていないアクセス", http.StatusUnauthorized)
			return
		}
		log.Printf("ユーザートークン: %s", token)
		next.ServeHTTP(w, r)
	})
}

func main() {
	mux := http.NewServeMux() // 💡 カスタムルーターを作成

	// 公開ルート
	mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
		fmt.Fprintf(w, "APIサービスへようこそ!")
	})

	mux.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) {
		fmt.Fprintf(w, `{"status": "ok"}`)
	})

	// 認証が必要なルート
	protected := http.NewServeMux()
	protected.HandleFunc("/profile", func(w http.ResponseWriter, r *http.Request) {
		fmt.Fprintf(w, "これはあなたのプロフィールページです")
	})
	protected.HandleFunc("/settings", func(w http.ResponseWriter, r *http.Request) {
		fmt.Fprintf(w, "これは設定ページです")
	})

	// 💡 ミドルウェアチェーン:ログ→認証→処理
	mux.Handle("/api/", AuthMiddleware(protected))

	// すべてのルートにログミドルウェアを適用
	finalHandler := LoggingMiddleware(mux)

	fmt.Println("サーバーが http://localhost:8080 で実行中")
	log.Fatal(http.ListenAndServe(":8080", finalHandler))
}
▶ 試してみよう

実行とテスト:

BASH
# 実行
go run main.go

# 公開ルートをテスト
curl http://localhost:8080/
curl http://localhost:8080/health

# 認証ルートをテスト(トークンなし)
curl http://localhost:8080/api/profile
# 出力: 認証されていないアクセス

# 認証ルートをテスト(トークン付き)
curl -H "Authorization: Bearer mytoken123" http://localhost:8080/api/profile
# 出力: これはあなたのプロフィールページです

例:完全なRESTful APIサービス(難易度⭐⭐⭐)

GO
package main

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

// Userモデル
type User struct {
	ID    int    `json:"id"`
	Name  string `json:"name"`
	Email string `json:"email"`
}

// APIResponse 統一レスポンス形式
type APIResponse struct {
	Code    int         `json:"code"`
	Message string      `json:"message"`
	Data    interface{} `json:"data,omitempty"`
}

// UserStore ユーザーストア(mapでデータベースをシミュレーション)
type UserStore struct {
	mu     sync.RWMutex
	users  map[int]*User
	nextID int
}

func NewUserStore() *UserStore {
	return &UserStore{
		users:  make(map[int]*User),
		nextID: 1,
	}
}

func (s *UserStore) Create(name, email string) *User {
	s.mu.Lock()
	defer s.mu.Unlock()

	user := &User{
		ID:    s.nextID,
		Name:  name,
		Email: email,
	}
	s.users[s.nextID] = user
	s.nextID++
	return user
}

func (s *UserStore) Get(id int) *User {
	s.mu.RLock()
	defer s.mu.RUnlock()
	return s.users[id]
}

func (s *UserStore) List() []*User {
	s.mu.RLock()
	defer s.mu.RUnlock()

	users := make([]*User, 0, len(s.users))
	for _, u := range s.users {
		users = append(users, u)
	}
	return users
}

func (s *UserStore) Update(id int, name, email string) *User {
	s.mu.Lock()
	defer s.mu.Unlock()

	user, ok := s.users[id]
	if !ok {
		return nil
	}
	if name != "" {
		user.Name = name
	}
	if email != "" {
		user.Email = email
	}
	return user
}

func (s *UserStore) Delete(id int) bool {
	s.mu.Lock()
	defer s.mu.Unlock()

	if _, ok := s.users[id]; !ok {
		return false
	}
	delete(s.users, id)
	return true
}

// UserHandler ユーザーAPIハンドラー
type UserHandler struct {
	store *UserStore
}

// writeJSON JSONレスポンスを書き込み
func (h *UserHandler) writeJSON(w http.ResponseWriter, code int, resp APIResponse) {
	w.Header().Set("Content-Type", "application/json")
	w.WriteHeader(code)
	json.NewEncoder(w).Encode(resp)
}

// List ユーザーリスト取得 GET /users
func (h *UserHandler) List(w http.ResponseWriter, r *http.Request) {
	users := h.store.List()
	h.writeJSON(w, http.StatusOK, APIResponse{
		Code:    0,
		Message: "success",
		Data:    users,
	})
}

// Create ユーザー作成 POST /users
func (h *UserHandler) Create(w http.ResponseWriter, r *http.Request) {
	var req struct {
		Name  string `json:"name"`
		Email string `json:"email"`
	}

	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
		h.writeJSON(w, http.StatusBadRequest, APIResponse{
			Code:    1,
			Message: "無効なリクエストデータ",
		})
		return
	}

	if req.Name == "" || req.Email == "" {
		h.writeJSON(w, http.StatusBadRequest, APIResponse{
			Code:    2,
			Message: "名前とメールアドレスは空にできません",
		})
		return
	}

	user := h.store.Create(req.Name, req.Email)
	h.writeJSON(w, http.StatusCreated, APIResponse{
		Code:    0,
		Message: "作成成功",
		Data:    user,
	})
}

// Get 単一ユーザー取得 GET /users/{id}
func (h *UserHandler) Get(w http.ResponseWriter, r *http.Request) {
	idStr := strings.TrimPrefix(r.URL.Path, "/users/")
	id, err := strconv.Atoi(idStr)
	if err != nil {
		h.writeJSON(w, http.StatusBadRequest, APIResponse{
			Code:    1,
			Message: "無効なユーザーID",
		})
		return
	}

	user := h.store.Get(id)
	if user == nil {
		h.writeJSON(w, http.StatusNotFound, APIResponse{
			Code:    3,
			Message: "ユーザーが見つかりません",
		})
		return
	}

	h.writeJSON(w, http.StatusOK, APIResponse{
		Code:    0,
		Message: "success",
		Data:    user,
	})
}

// Update ユーザー更新 PUT /users/{id}
func (h *UserHandler) Update(w http.ResponseWriter, r *http.Request) {
	idStr := strings.TrimPrefix(r.URL.Path, "/users/")
	id, err := strconv.Atoi(idStr)
	if err != nil {
		h.writeJSON(w, http.StatusBadRequest, APIResponse{
			Code:    1,
			Message: "無効なユーザーID",
		})
		return
	}

	var req struct {
		Name  string `json:"name"`
		Email string `json:"email"`
	}
	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
		h.writeJSON(w, http.StatusBadRequest, APIResponse{
			Code:    1,
			Message: "無効なリクエストデータ",
		})
		return
	}

	user := h.store.Update(id, req.Name, req.Email)
	if user == nil {
		h.writeJSON(w, http.StatusNotFound, APIResponse{
			Code:    3,
			Message: "ユーザーが見つかりません",
		})
		return
	}

	h.writeJSON(w, http.StatusOK, APIResponse{
		Code:    0,
		Message: "更新成功",
		Data:    user,
	})
}

// Delete ユーザー削除 DELETE /users/{id}
func (h *UserHandler) Delete(w http.ResponseWriter, r *http.Request) {
	idStr := strings.TrimPrefix(r.URL.Path, "/users/")
	id, err := strconv.Atoi(idStr)
	if err != nil {
		h.writeJSON(w, http.StatusBadRequest, APIResponse{
			Code:    1,
			Message: "無効なユーザーID",
		})
		return
	}

	if !h.store.Delete(id) {
		h.writeJSON(w, http.StatusNotFound, APIResponse{
			Code:    3,
			Message: "ユーザーが見つかりません",
		})
		return
	}

	h.writeJSON(w, http.StatusOK, APIResponse{
		Code:    0,
		Message: "削除成功",
	})
}

// CORSミドルウェア
func CORSMiddleware(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		w.Header().Set("Access-Control-Allow-Origin", "*")
		w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
		w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization")

		if r.Method == "OPTIONS" {
			w.WriteHeader(http.StatusOK)
			return
		}

		next.ServeHTTP(w, r)
	})
}

// Recoveryミドルウェア:パニックをキャッチ
func RecoveryMiddleware(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		defer func() {
			if err := recover(); err != nil {
				log.Printf("パニックをキャッチ: %v", err)
				http.Error(w, "内部サーバーエラー", http.StatusInternalServerError)
			}
		}()
		next.ServeHTTP(w, r)
	})
}

func main() {
	store := NewUserStore()
	handler := &UserHandler{store: store}

	// ルーターを作成
	mux := http.NewServeMux()

	// 💡 ルートを登録:HTTPメソッドに基づいて異なるハンドラーにディスパッチ
	mux.HandleFunc("/users", func(w http.ResponseWriter, r *http.Request) {
		switch r.Method {
		case http.MethodGet:
			handler.List(w, r)
		case http.MethodPost:
			handler.Create(w, r)
		default:
			http.Error(w, "許可されていないメソッド", http.StatusMethodNotAllowed)
		}
	})

	// 💡 パスパラメータ付きルートを処理 /users/{id}
	mux.HandleFunc("/users/", func(w http.ResponseWriter, r *http.Request) {
		switch r.Method {
		case http.MethodGet:
			handler.Get(w, r)
		case http.MethodPut:
			handler.Update(w, r)
		case http.MethodDelete:
			handler.Delete(w, r)
		default:
			http.Error(w, "許可されていないメソッド", http.StatusMethodNotAllowed)
		}
	})

	// ミドルウェアチェーンを適用
	finalHandler := RecoveryMiddleware(CORSMiddleware(mux))

	// サーバーを作成(タイムアウト設定可能)
	server := &http.Server{
		Addr:         ":8080",
		Handler:      finalHandler,
		ReadTimeout:  10 * time.Second,  // 💡 読み取りタイムアウト
		WriteTimeout: 10 * time.Second,  // 💡 書き込みタイムアウト
	}

	// テストデータを挿入
	store.Create("Alice", "alice@example.com")
	store.Create("Bob", "bob@example.com")

	fmt.Println("RESTful APIサーバーが http://localhost:8080 で実行中")
	fmt.Println("利用可能なエンドポイント:")
	fmt.Println("  GET    /users      - ユーザーリスト取得")
	fmt.Println("  POST   /users      - ユーザー作成")
	fmt.Println("  GET    /users/{id} - 単一ユーザー取得")
	fmt.Println("  PUT    /users/{id} - ユーザー更新")
	fmt.Println("  DELETE /users/{id} - ユーザー削除")

	log.Fatal(server.ListenAndServe())
}
▶ 試してみよう

実行とフルテスト:

BASH
# サーバーを実行
go run main.go

# ユーザーリストを取得
curl http://localhost:8080/users

# ユーザーを作成
curl -X POST http://localhost:8080/users \
  -H "Content-Type: application/json" \
  -d '{"name": "Charlie", "email": "charlie@example.com"}'

# 単一ユーザーを取得
curl http://localhost:8080/users/1

# ユーザーを更新
curl -X PUT http://localhost:8080/users/1 \
  -H "Content-Type: application/json" \
  -d '{"name": "Alice Smith"}'

# ユーザーを削除
curl -X DELETE http://localhost:8080/users/3

期待出力例:

TEXT
# GET /users
{"code":0,"message":"success","data":[{"id":1,"name":"Alice","email":"alice@example.com"},{"id":2,"name":"Bob","email":"bob@example.com"}]}

# POST /users
{"code":0,"message":"作成成功","data":{"id":3,"name":"Charlie","email":"charlie@example.com"}}

# GET /users/1
{"code":0,"message":"success","data":{"id":1,"name":"Alice","email":"alice@example.com"}}

🎬 シナリオウォークスルー

シナリオ1:天気照会サービスの構築

GO
package main

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

// WeatherResponse シミュレーション用天気APIレスポンス
type WeatherResponse struct {
	City        string  `json:"city"`
	Temperature float64 `json:"temperature"`
	Condition   string  `json:"condition"`
	Humidity    int     `json:"humidity"`
}

// WeatherService 天気サービスハンドラー
type WeatherService struct {
	// シミュレーションデータ
	data map[string]WeatherResponse
}

func NewWeatherService() *WeatherService {
	return &WeatherService{
		data: map[string]WeatherResponse{
			"beijing":  {City: "北京", Temperature: 28.5, Condition: "晴れ", Humidity: 45},
			"shanghai": {City: "上海", Temperature: 30.2, Condition: "曇り", Humidity: 65},
			"guangzhou": {City: "広州", Temperature: 33.1, Condition: "雷雨", Humidity: 80},
		},
	}
}

// GetWeather 天気を照会 GET /weather?city=beijing
func (s *WeatherService) GetWeather(w http.ResponseWriter, r *http.Request) {
	// 💡 URLクエリパラメータから都市名を取得
	city := r.URL.Query().Get("city")
	if city == "" {
		http.Error(w, `{"error": "cityパラメータを指定してください"}`, http.StatusBadRequest)
		return
	}

	// URLデコード(中国語都市名サポート)
	city, _ = url.QueryUnescape(city)

	// 天気データを検索(シミュレーション)
	weather, ok := s.data[city]
	if !ok {
		http.Error(w, fmt.Sprintf(`{"error": "都市が見つかりません: %s"}`, city), http.StatusNotFound)
		return
	}

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

// GetForecast 予報リスト GET /forecast
func (s *WeatherService) GetForecast(w http.ResponseWriter, r *http.Request) {
	w.Header().Set("Content-Type", "application/json")

	// すべての都市の天気を返す
	forecasts := make([]WeatherResponse, 0, len(s.data))
	for _, v := range s.data {
		forecasts = append(forecasts, v)
	}

	json.NewEncoder(w).Encode(map[string]interface{}{
		"count":     len(forecasts),
		"forecasts": forecasts,
	})
}

func main() {
	service := NewWeatherService()

	mux := http.NewServeMux()
	mux.HandleFunc("/weather", service.GetWeather)
	mux.HandleFunc("/forecast", service.GetForecast)

	fmt.Println("天気サービスが http://localhost:8080 で実行中")
	fmt.Println("使い方:")
	fmt.Println("  GET /weather?city=beijing")
	fmt.Println("  GET /forecast")

	http.ListenAndServe(":8080", mux)
}
BASH
# テスト
curl "http://localhost:8080/weather?city=beijing"
# {"city":"北京","temperature":28.5,"condition":"晴れ","humidity":45}

curl http://localhost:8080/forecast
# {"count":3,"forecasts":[...]}

シナリオ2:レート制限ミドルウェアの実装

GO
package main

import (
	"fmt"
	"net/http"
	"sync"
	"time"
)

// RateLimiter トークンバケットレートリミッター
type RateLimiter struct {
	mu       sync.Mutex
	tokens   map[string]int
	limit    int
	interval time.Duration
	lastTick map[string]time.Time
}

func NewRateLimiter(limit int, interval time.Duration) *RateLimiter {
	return &RateLimiter{
		tokens:   make(map[string]int),
		limit:    limit,
		interval: interval,
		lastTick: make(map[string]time.Time),
	}
}

// Allow リクエストが許可されるかチェック
func (rl *RateLimiter) Allow(key string) bool {
	rl.mu.Lock()
	defer rl.mu.Unlock()

	now := time.Now()
	last, exists := rl.lastTick[key]

	if !exists || now.Sub(last) >= rl.interval {
		// 💡 トークンをリセット
		rl.tokens[key] = rl.limit
		rl.lastTick[key] = now
	}

	if rl.tokens[key] <= 0 {
		return false
	}

	rl.tokens[key]--
	return true
}

// RateLimitMiddleware レート制限ミドルウェア
func RateLimitMiddleware(limiter *RateLimiter) func(http.Handler) http.Handler {
	return func(next http.Handler) http.Handler {
		return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
			// クライアントIPをレート制限キーとして使用
			clientIP := r.RemoteAddr

			if !limiter.Allow(clientIP) {
				w.Header().Set("Content-Type", "application/json")
				w.WriteHeader(http.StatusTooManyRequests) // 429
				fmt.Fprintf(w, `{"error": "リクエストが多すぎます。しばらくしてからお試しください"}`)
				return
			}

			next.ServeHTTP(w, r)
		})
	}
}

func main() {
	// レートリミッターを作成:IPごとに1分あたり最大10リクエスト
	limiter := NewRateLimiter(10, time.Minute)

	mux := http.NewServeMux()

	mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
		fmt.Fprintf(w, "リクエスト成功!時刻: %s", time.Now().Format("15:04:05"))
	})

	mux.HandleFunc("/api/data", func(w http.ResponseWriter, r *http.Request) {
		fmt.Fprintf(w, `{"data": "これはAPIデータです", "time": "%s"}`, time.Now().Format("15:04:05"))
	})

	// レート制限ミドルウェアを適用
	handler := RateLimitMiddleware(limiter)(mux)

	fmt.Println("レート制限サーバーが http://localhost:8080 で実行中")
	fmt.Println("レート制限ルール: 1分あたり最大10リクエスト")
	http.ListenAndServe(":8080", handler)
}
BASH
# レート制限をテスト(素早く複数のリクエストを送信)
for i in {1..15}; do
  curl -s http://localhost:8080/
  echo ""
done
# 最初の10リクエストは正常に返され、それ以降は429エラーが返される

❓ よくある質問

質問1:http.HandleFunchttp.Handleの違いは何ですか?

GO
// HandleFuncは関数を受け取る
http.HandleFunc("/path", func(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintf(w, "Hello")
})

// HandleはHandlerインターフェースを受け取る
type MyHandler struct{}
func (h MyHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintf(w, "Hello")
}
http.Handle("/path", MyHandler{})

違い:


質問2:ハンドラー関数でリクエストパラメータを取得するにはどうすればいいですか?

GO
func handler(w http.ResponseWriter, r *http.Request) {
	// 💡 クエリパラメータ(URLの?key=value)
	name := r.URL.Query().Get("name")

	// 💡 フォームデータ(POST form-urlencoded)
	r.ParseForm()
	email := r.Form.Get("email")

	// 💡 パスパラメータ(手動解析またはサードパーティルーターが必要)
	// 例:/users/123の123
	path := r.URL.Path // "/users/123"
	// 手動解析:strings.TrimPrefix(path, "/users/")

	// 💡 リクエストヘッダー
	token := r.Header.Get("Authorization")

	// 💡 JSONリクエストボディ
	var data map[string]string
	json.NewDecoder(r.Body).Decode(&data)

	fmt.Fprintf(w, "name=%s, email=%s, token=%s", name, email, token)
}

質問3:サーバーのグレースフルシャットダウンをどう実装すればいいですか?

GO
package main

import (
	"context"
	"fmt"
	"log"
	"net/http"
	"os"
	"os/signal"
	"syscall"
	"time"
)

func main() {
	server := &http.Server{
		Addr: ":8080",
		Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
			time.Sleep(2 * time.Second) // 時間のかかる操作をシミュレーション
			fmt.Fprintf(w, "処理完了")
		}),
	}

	// goroutineでサーバーを起動
	go func() {
		fmt.Println("サーバーが :8080 で起動しました")
		if err := server.ListenAndServe(); err != http.ErrServerClosed {
			log.Fatalf("サーバーエラー: %v", err)
		}
	}()

	// 💡 システムシグナルをリッスン
	quit := make(chan os.Signal, 1)
	signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
	<-quit // ブロックしてシグナルを待機

	fmt.Println("\nサーバーをグレースフルにシャットダウン中...")

	// 💡 既存のリクエストに5秒の猶予を与える
	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
	defer cancel()

	if err := server.Shutdown(ctx); err != nil {
		log.Fatalf("シャットダウン失敗: %v", err)
	}

	fmt.Println("サーバーが安全にシャットダウンしました")
}
BASH
# 実行後、Ctrl+Cでグレースフルシャットダウンをトリガー
go run main.go
# 出力: サーバーが :8080 で起動しました
# Ctrl+Cを押す
# 出力: サーバーをグレースフルにシャットダウン中...
# 出力: サーバーが安全にシャットダウンしました

質問4:なぜListenAndServeの第2パラメータにカスタムMuxを渡すことが推奨されるのですか?

GO
// ❌ デフォルトのDefaultServeMuxを使用(非推奨)
http.HandleFunc("/api/users", usersHandler)
http.ListenAndServe(":8080", nil) // nilはDefaultServeMuxを使用することを意味する

// ✅ カスタムMuxを使用(推奨)
mux := http.NewServeMux()
mux.HandleFunc("/api/users", usersHandler)
http.ListenAndServe(":8080", mux)

理由:

  1. DefaultServeMuxはグローバル — どのパッケージもルートを登録でき、競合が発生する可能性がある
  2. カスタムMuxはスコープが制御可能で、予期しないルートの上書きを回避
  3. 異なるサーバーインスタンス間で再利用とテストが容易

📖 まとめ

このレッスンではGoのHTTPプログラミングのコア内容をカバーしました:

知識ポイント 重要なポイント
HTTPクライアント http.Get/Postでリクエスト送信、resp.Bodyは必ず閉じる
HTTPサーバー http.ListenAndServeでサービス起動、ルート登録
Handlerインターフェース ServeHTTP(ResponseWriter, *Request)メソッド
HandlerFunc 関数アダプター、関数をHandlerに変換
ServeMux ルーター、プレフィックスマッチングをサポート
ミドルウェア Handlerをラップし、リクエスト前後に共通ロジックを挿入
グレースフルシャットダウン server.Shutdownとシグナル処理を使用

コアパターン:

TEXT
クライアントリクエスト → ミドルウェアチェーン → ルートマッチング → ハンドラー処理 → レスポンス返却

📝 演習

演習1:ファイルアップロードサービスの実装

要件:

参考解答
GO
package main

import (
	"fmt"
	"io"
	"net/http"
	"os"
	"path/filepath"
	"strings"
)

func uploadHandler(w http.ResponseWriter, r *http.Request) {
	if r.Method != http.MethodPost {
		http.Error(w, "POSTメソッドのみサポートしています", http.StatusMethodNotAllowed)
		return
	}

	// 💡 アップロードサイズを10MBに制限
	r.ParseMultipartForm(10 << 20)

	file, header, err := r.FormFile("file")
	if err != nil {
		http.Error(w, "ファイルの取得に失敗しました", http.StatusBadRequest)
		return
	}
	defer file.Close()

	// ファイルタイプをチェック
	ext := strings.ToLower(filepath.Ext(header.Filename))
	allowed := map[string]bool{".jpg": true, ".jpeg": true, ".png": true, ".gif": true}
	if !allowed[ext] {
		http.Error(w, "画像ファイルのみ許可されています", http.StatusBadRequest)
		return
	}

	// ファイルを保存
	os.MkdirAll("./uploads", 0755)
	dst, err := os.Create(filepath.Join("./uploads", header.Filename))
	if err != nil {
		http.Error(w, "ファイルの保存に失敗しました", http.StatusInternalServerError)
		return
	}
	defer dst.Close()

	io.Copy(dst, file)

	fmt.Fprintf(w, `{"url": "/files/%s", "size": %d}`, header.Filename, header.Size)
}

func main() {
	http.HandleFunc("/upload", uploadHandler)
	http.Handle("/files/", http.StripPrefix("/files/", http.FileServer(http.Dir("./uploads"))))

	fmt.Println("ファイルアップロードサービスが http://localhost:8080 で実行中")
	http.ListenAndServe(":8080", nil)
}

演習2:JWT認証ミドルウェアの実装

要件:

参考解答
GO
package main

import (
	"encoding/base64"
	"encoding/json"
	"fmt"
	"net/http"
	"strings"
	"time"
)

// 簡略化されたJWT構造
type SimpleToken struct {
	Username string    `json:"username"`
	ExpireAt time.Time `json:"expire"`
}

// シンプルなトークンを生成
func generateToken(username string) (string, error) {
	token := SimpleToken{
		Username: username,
		ExpireAt: time.Now().Add(1 * time.Hour),
	}
	data, err := json.Marshal(token)
	if err != nil {
		return "", err
	}
	return base64.StdEncoding.EncodeToString(data), nil
}

// トークンを検証
func validateToken(tokenStr string) (*SimpleToken, error) {
	data, err := base64.StdEncoding.DecodeString(tokenStr)
	if err != nil {
		return nil, fmt.Errorf("無効なトークン")
	}

	var token SimpleToken
	if err := json.Unmarshal(data, &token); err != nil {
		return nil, fmt.Errorf("トークンの解析に失敗しました")
	}

	if time.Now().After(token.ExpireAt) {
		return nil, fmt.Errorf("トークンの有効期限が切れています")
	}

	return &token, nil
}

// 認証ミドルウェア
func AuthMiddleware(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		auth := r.Header.Get("Authorization")
		if auth == "" {
			http.Error(w, `{"error": "Authorizationヘッダーを指定してください"}`, http.StatusUnauthorized)
			return
		}

		// "Bearer <token>"を解析
		parts := strings.SplitN(auth, " ", 2)
		if len(parts) != 2 || parts[0] != "Bearer" {
			http.Error(w, `{"error": "無効なAuthorization形式"}`, http.StatusUnauthorized)
			return
		}

		token, err := validateToken(parts[1])
		if err != nil {
			http.Error(w, fmt.Sprintf(`{"error": "%s"}`, err), http.StatusUnauthorized)
			return
		}

		// ユーザー情報をリクエストヘッダーに格納(簡略化)
		r.Header.Set("X-Username", token.Username)
		next.ServeHTTP(w, r)
	})
}

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

	// ログインエンドポイント
	mux.HandleFunc("/login", func(w http.ResponseWriter, r *http.Request) {
		if r.Method != http.MethodPost {
			http.Error(w, "POSTのみサポートしています", http.StatusMethodNotAllowed)
			return
		}

		var req struct {
			Username string `json:"username"`
			Password string `json:"password"`
		}
		json.NewDecoder(r.Body).Decode(&req)

		// シンプルなバリデーション(本番ではデータベースを確認)
		if req.Username != "admin" || req.Password != "123456" {
			http.Error(w, `{"error": "ユーザー名またはパスワードが無効です"}`, http.StatusUnauthorized)
			return
		}

		token, _ := generateToken(req.Username)
		fmt.Fprintf(w, `{"token": "%s"}`, token)
	})

	// 保護されたエンドポイント
	protected := http.NewServeMux()
	protected.HandleFunc("/protected", func(w http.ResponseWriter, r *http.Request) {
		username := r.Header.Get("X-Username")
		fmt.Fprintf(w, `{"message": "ようこそ %s、これは保護されたコンテンツです"}`, username)
	})

	mux.Handle("/protected", AuthMiddleware(protected))

	fmt.Println("JWT認証サービスが http://localhost:8080 で実行中")
	http.ListenAndServe(":8080", mux)
}
BASH
# トークンを取得
curl -X POST http://localhost:8080/login \
  -H "Content-Type: application/json" \
  -d '{"username": "admin", "password": "123456"}'

# トークンを使用して保護されたリソースにアクセス
curl -H "Authorization: Bearer <token>" http://localhost:8080/protected

演習3:並行安全なAPIレートリミッターの構築

要件:

参考解答
GO
package main

import (
	"fmt"
	"net/http"
	"sync"
	"time"
)

// SlidingWindowLimiter スライディングウィンドウレートリミッター
type SlidingWindowLimiter struct {
	mu       sync.Mutex
	windows  map[string]*window
	limit    int
	interval time.Duration
}

type window struct {
	count     int
	startTime time.Time
}

func NewSlidingWindowLimiter(limit int, interval time.Duration) *SlidingWindowLimiter {
	return &SlidingWindowLimiter{
		windows:  make(map[string]*window),
		limit:    limit,
		interval: interval,
	}
}

func (l *SlidingWindowLimiter) Allow(key string) bool {
	l.mu.Lock()
	defer l.mu.Unlock()

	now := time.Now()
	w, exists := l.windows[key]

	if !exists || now.Sub(w.startTime) >= l.interval {
		// 新しいウィンドウを開く
		l.windows[key] = &window{count: 1, startTime: now}
		return true
	}

	if w.count >= l.limit {
		return false
	}

	w.count++
	return true
}

// 期限切れウィンドウをクリーンアップ(定期的に呼び出す必要がある)
func (l *SlidingWindowLimiter) Cleanup() {
	l.mu.Lock()
	defer l.mu.Unlock()

	now := time.Now()
	for key, w := range l.windows {
		if now.Sub(w.startTime) >= l.interval*2 {
			delete(l.windows, key)
		}
	}
}

func SlidingWindowMiddleware(limiter *SlidingWindowLimiter) func(http.Handler) http.Handler {
	return func(next http.Handler) http.Handler {
		return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
			if !limiter.Allow(r.RemoteAddr) {
				w.Header().Set("Retry-After", "60")
				http.Error(w, `{"error": "リクエストが多すぎます"}`, http.StatusTooManyRequests)
				return
			}
			next.ServeHTTP(w, r)
		})
	}
}

func main() {
	limiter := NewSlidingWindowLimiter(5, time.Minute) // 1分あたり5回

	// 定期的なクリーンアップ
	go func() {
		for {
			time.Sleep(10 * time.Minute)
			limiter.Cleanup()
		}
	}()

	mux := http.NewServeMux()
	mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
		fmt.Fprintf(w, "リクエスト成功: %s", time.Now().Format("15:04:05"))
	})

	handler := SlidingWindowMiddleware(limiter)(mux)

	fmt.Println("スライディングウィンドウレートリミッターが http://localhost:8080 で実行中")
	fmt.Println("レート制限ルール: 1分あたり5リクエスト")
	http.ListenAndServe(":8080", handler)
}

📚 次のレッスン

HTTPプログラミングを完了しました!次のレッスンではGoのテストプログラミングについて学びます。単体テスト、テーブル駆動テスト、ベンチマークテストなど、より信頼性の高いコードを書くためのスキルを身につけます。

次のレッスン:テスト →

Web-Tutorial.com

Web-Tutorial 技術チーム

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

100%