HTTPプログラミング
レッスン22:HTTPプログラミング
🎯 生活での例え
あなたがレストランを経営していると想像してください:
- HTTPサーバーはレストランの受付係のようなもの — お客さん(クライアント)が来ると、受付係はニーズに基づいて正しいサービスクウンターに案内します
- ルーティング(ServeMux)はレストランのメニューのカテゴリのようなもの — 異なる料理(URLパス)は異なるシェフ(ハンドラー関数)が処理します
- Handlerインターフェースはシェフの作業基準のようなもの — どの料理を準備するかに関わらず、統一されたワークフローに従う必要があります
- ミドルウェアはレストランのサービスプロセスのようなもの — お客さんが靴を脱ぎ、手を洗い、然后座る。これらのステップはすべての人に共通で、特定の料理に固有ではありません
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)
}
💡 ヒント:
HandleはHandlerインターフェースを受け取り、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.HandleFuncとhttp.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{})
違い:
HandleFunc:func(ResponseWriter, *Request)型の関数を受け取るHandle:Handlerインターフェースを実装する型を受け取るHandleFuncは内部的に関数をHandlerFuncアダプターでラップする
質問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)
理由:
DefaultServeMuxはグローバル — どのパッケージもルートを登録でき、競合が発生する可能性がある- カスタムMuxはスコープが制御可能で、予期しないルートの上書きを回避
- 異なるサーバーインスタンス間で再利用とテストが容易
📖 まとめ
このレッスンではGoのHTTPプログラミングのコア内容をカバーしました:
| 知識ポイント | 重要なポイント |
|---|---|
| HTTPクライアント | http.Get/Postでリクエスト送信、resp.Bodyは必ず閉じる |
| HTTPサーバー | http.ListenAndServeでサービス起動、ルート登録 |
| Handlerインターフェース | ServeHTTP(ResponseWriter, *Request)メソッド |
| HandlerFunc | 関数アダプター、関数をHandlerに変換 |
| ServeMux | ルーター、プレフィックスマッチングをサポート |
| ミドルウェア | Handlerをラップし、リクエスト前後に共通ロジックを挿入 |
| グレースフルシャットダウン | server.Shutdownとシグナル処理を使用 |
コアパターン:
TEXT
クライアントリクエスト → ミドルウェアチェーン → ルートマッチング → ハンドラー処理 → レスポンス返却
📝 演習
演習1:ファイルアップロードサービスの実装
要件:
POST /uploadエンドポイントでファイルアップロードを受信- ファイルサイズを10MBに制限
- 画像タイプのみ許可(jpg、png、gif)
- ファイルを
./uploadsディレクトリに保存 - ファイルのアクセスURLを返す
参考解答
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認証ミドルウェアの実装
要件:
- シンプルなJWT生成と検証を実装(Base64でシミュレーション可)
POST /loginエンドポイントでトークンを取得- 保護された
GET /protectedエンドポイントを作成 Authorization: Bearer <token>でトークンを渡す
参考解答
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レートリミッターの構築
要件:
- スライディングウィンドウレート制限アルゴリズムを実装
- ウィンドウあたりの最大リクエスト数とウィンドウサイズの設定をサポート
- ミドルウェアインターフェースを提供
- Redisまたはインメモリストレージを使用(インメモリで可)
参考解答
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のテストプログラミングについて学びます。単体テスト、テーブル駆動テスト、ベンチマークテストなど、より信頼性の高いコードを書くためのスキルを身につけます。



