GoのHTTPクライアントとミドルウェア
HTTPクライアントとミドルウェアは、マイクロサービス間の通信を支える2つの要です。一方は接続を管理し、もう一方は横断的関心事を処理します。
自分のサービスが他の5つの下流サービスを呼び出す必要がある場合、各呼び出しにはログ記録、タイムアウト処理、再試行、サーキットブレーカーが必要です。このコードを5回書くべきでしょうか、それとも一度抽象化すべきでしょうか?このレッスンでは、GoのHTTPクライアントとミドルウェアの設計パターンの高度な使い方を習得します。
1. 学習内容
http.Get/Post/Do基本的なリクエストhttp.Clientタイムアウト制御http.Transport接続プールの設定- ミドルウェアパターン(オニオンモデル)
- 一般的なミドルウェア:ロギング、リカバリ、CORS、タイムアウト
httputil.ReverseProxyリバースプロキシ
2. バックエンドエンジニアの実話
(1) 課題:5つの下流サービスがあり、それぞれでログ記録、タイムアウト処理、再試行が必要である
チャーリーは、同社のAPIゲートウェイサービスの責任者を務めており、このサービスでは5つの下流サービスを呼び出す必要があります:
「下流の呼び出しはすべて、リクエストログを出力し(トラブルシューティングを容易にするため)、タイムアウトを設定し(デッドロックを防ぐため)、所要時間を記録しなければならない(監視やアラートのため)。結局、同じコードを5回も書くことになった。サービスごとに1つの関数を用意し、それぞれにログ出力、タイムアウト設定、そして呼び出し本体の3つを「サンドイッチ」のように挟んだ、同じ繰り返し処理を含める形になった。」
当時の彼のコード:
// 不良コード:各下流サービスで同じロジックが繰り返される
func callUserService(w http.ResponseWriter, r *http.Request) {
log.Printf("[%s] ユーザーサービスのリクエスト: %s", r.Method, r.URL.Path)
start := time.Now()
resp, err := http.Get("http://user-service/api/users")
log.Printf("[%s] ユーザーサービスの所要時間: %v", r.Method, time.Since(start))
// ... レスポンスの処理
}
func callOrderService(w http.ResponseWriter, r *http.Request) {
log.Printf("[%s] 注文サービスのリクエスト: %s", r.Method, r.URL.Path)
start := time.Now()
resp, err := http.Get("http://order-service/api/orders")
log.Printf("[%s] 注文処理にかかる時間: %v", r.Method, time.Since(start))
// ... また同じパターンだ!
}
// 新しいサービスごとに繰り返される!
(2) Goによる解決策:ミドルウェアパターン+カスタムクライアント
// ミドルウェア:包装 http.RoundTripper
type LoggingMiddleware struct {
next http.RoundTripper
}
func (m *LoggingMiddleware) RoundTrip(req *http.Request) (*http.Response, エラー) {
start := time.Now()
log.Printf("[%s] %s %s", req.Method, req.URL.Host, req.URL.Path)
resp, err := m.next.RoundTrip(req)
log.Printf("[%s] %s 所要時間: %v", req.Method, req.URL.Path, time.Since(start))
return resp, err
}
// 统一 Client:すべての下流で共有
クライアント := &http.Client{
Timeout: 5 * time.Second,
Transport: &LoggingMiddleware{
next: http.DefaultTransport,
},
}
// すべての下流呼び出しに対して自動的にログが記録される + 超时
resp1, _ := クライアント.Get("http://ユーザー-service/api/users") // 自动日志
resp2, _ := クライアント.Get("http://順序-service/api/orders") // 自动日志
(3) メリット:ミドルウェア導入前と導入後
| 次元 | 重複コード | ミドルウェアパターン |
|---|---|---|
| コード量 | ダウンストリームごとに30行、ダウンストリームが5つ=150行 | ミドルウェアが15行、合計30行 |
| 下流に追加 | 30行をコピーして貼り付け | client.Get を直接呼び出す |
| 編集タイムアウト | 5か所を変更 | 1つのクライアントのタイムアウトを変更 |
3. HTTPクライアントの基礎
▶ サンプル:GET / POST / カスタムリクエスト
package main
import (
"bytes"
"encoding/json"
"fmt"
"io"
"log"
"net/http"
)
func main() {
// 1. GET 请求
resp, err := http.Get("https://api.example.com/users")
if err != nil {
log.Fatal(err)
}
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)
fmt.Printf("GET ステータスコード: %d\n", resp.StatusCode)
fmt.Printf("応答: %s\n", body)
// 2. POST JSON 请求
data := map[string]string{"name": "Alice"}
jsonData, _ := json.Marshal(data)
resp, err = http.Post(
"https://api.example.com/users",
"application/json",
bytes.NewReader(jsonData),
)
if err != nil {
log.Fatal(err)
}
defer resp.Body.Close()
// 3. カスタムリクエスト(設定 Header)
req, _ := http.NewRequest("DELETE", "https://api.example.com/users/1", nil)
req.Header.Set("Authorization", "Bearer token-123")
req.Header.Set("X-Request-ID", "req-456")
resp, err = http.DefaultClient.Do(req)
if err != nil {
log.Fatal(err)
}
defer resp.Body.Close()
fmt.Printf("DELETE ステータスコード: %d\n", resp.StatusCode)
}
(1) http.Get と http.Client
// 方法 1:そのまま使用 http.Get(デフォルト Client)
resp, _ := http.Get(url)
// 問題:デフォルトではタイムアウトは設定されていません,可能永远挂起!
// 方法 2:カスタマイズ Client(推荐)
client := &http.Client{
Timeout: 10 * time.Second,
}
resp, _ := client.Get(url)
// 安全:10 タイムアウトすると自動的にキャンセルされる
| メソッド | タイムアウト | 接続プール | 推奨事項 |
|---|---|---|---|
http.Get(url) |
タイムアウトなし | デフォルト(再利用に制限あり) | ❌ 簡易テスト用のみ |
http.DefaultClient |
タイムアウトなし | デフォルト | ⚠️ 本番環境では注意して使用してください |
&http.Client{Timeout: 10s} |
グローバルタイムアウト | デフォルト | ✅ 推奨 |
&http.Client{Timeout, Transport} |
グローバルタイムアウト | カスタム接続プール | ✅ 本番環境対応 |
4. http.Client のタイムアウトとトランスポート
▶ サンプル:タイムアウトと接続プールの設定
パッケージ main
import (
"fmt"
"net"
"net/http"
"time"
)
func main() {
// カスタマイズ Transport
transport := &http.Transport{
// 连接池
MaxIdleConns: 100, // 最大空き接続数
MaxIdleConnsPerHost: 10, // 各ホストの最大アイドル接続数
IdleConnTimeout: 90 * time.Second, // アイドル接続のタイムアウト
// TLS
TLSHandshakeTimeout: 10 * time.Second,
// 結合
DialContext: (&net.Dialer{
Timeout: 30 * time.Second, // 连接超时
KeepAlive: 30 * time.Second, // Keep-Alive 間隔
}).DialContext,
}
// カスタマイズ Client
クライアント := &http.Client{
Timeout: 30 * time.Second, // リクエストの合計タイムアウト(すべての段階を含む)
Transport: transport,
}
// 使用 クライアント リクエストを送信
resp, err := クライアント.Get("https://api.example.com/users")
if err != nil {
fmt.Printf("リクエストが失敗しました: %v\n", err)
return
}
defer resp.Body.Close()
fmt.Printf("ステータスコード: %d\n", resp.StatusCode)
}
▶ サンプル:段階的なタイムアウト制御
package main
import (
"fmt"
"net"
"net/http"
"time"
)
func main() {
// 超時間階層化:
// Transport.DialContext → 连接超时(10s)
// Transport.TLSHandshake → TLS ハンドシェイクのタイムアウト(5s)
// Transport.ResponseHeader → 响应头超时(10s)
// Client.Timeout → 合計タイムアウト(30s,カバー Dial + TLS + 送信 + 読み込み)
transport := &http.Transport{
DialContext: (&net.Dialer{
Timeout: 10 * time.Second,
}).DialContext,
TLSHandshakeTimeout: 5 * time.Second,
ResponseHeaderTimeout: 10 * time.Second,
ExpectContinueTimeout: 1 * time.Second,
}
client := &http.Client{
Timeout: 30 * time.Second,
Transport: transport,
}
start := time.Now()
resp, err := client.Get("https://httpbin.org/delay/5")
if err != nil {
fmt.Printf("エラー: %v (所要時間: %v)\n", err, time.Since(start))
return
}
defer resp.Body.Close()
fmt.Printf("成功: %d (所要時間: %v)\n", resp.StatusCode, time.Since(start))
}
(2) 転送設定パラメータ
| パラメータ | デフォルト | 推奨値 | 説明 |
|---|---|---|---|
MaxIdleConns |
100 | 100–200 | アイドル接続の最大数 |
MaxIdleConnsPerHost |
2 | 10–50 | ホストあたりのアイドル接続の最大数(デフォルトの 2 は少なすぎます!) |
IdleConnTimeout |
90s | 30–90s | アイドル接続タイムアウト |
TLSHandshakeTimeout |
10秒 | 5~10秒 | TLSハンドシェイクのタイムアウト |
ResponseHeaderTimeout |
0 (なし) | 10~30秒 | レスポンスヘッダーのタイムアウト |
DialContext.Timeout |
なし | 10~30秒 | TCP接続タイムアウト |
http.Transport 内の MaxIdleConnsPerHost のデフォルト値は 2 のみですが、これは同じサービスへの高同時アクセスが発生するシナリオでは著しく不十分です。同時アクセス数に応じて、この値を必ず増やしてください(例:50~100)。
5. ミドルウェア・パターン
▶ サンプル:ミドルウェアのオニオンモデル
package main
import (
"fmt"
"log"
"net/http"
"time"
)
// ミドルウェアの種類:受信 Handler,戻る Handler
type Middleware func(http.Handler) http.Handler
// ミドルウェアチェーン:すべての中間ウェアを1つにまとめる
func Chain(handler http.Handler, middlewares ...Middleware) http.Handler {
for i := len(middlewares) - 1; i >= 0; i-- {
handler = middlewares[i](handler)
}
return handler
}
// ---------- 具体的なミドルウェア ----------
// 1. 日志中间件
func LoggingMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
log.Printf("[%s] %s %s", r.Method, r.URL.Path, r.RemoteAddr)
next.ServeHTTP(w, r)
log.Printf("[%s] %s 所要時間: %v", r.Method, r.URL.Path, time.Since(start))
})
}
// 2. ミドルウェアの復旧(防止 panic サービスのクラッシュを引き起こす)
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("[PANIC] %v", err)
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
}
}()
next.ServeHTTP(w, r)
})
}
// 3. 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 == http.MethodOptions {
w.WriteHeader(http.StatusNoContent)
return
}
next.ServeHTTP(w, r)
})
}
// 4. タイムアウトミドルウェア
func TimeoutMiddleware(timeout time.Duration) Middleware {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ctx, cancel := context.WithTimeout(r.Context(), timeout)
defer cancel()
next.ServeHTTP(w, r.WithContext(ctx))
})
}
}
// ---------- 使用 ----------
func helloHandler(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Hello, %s!", r.URL.Query().Get("name"))
}
func main() {
mux := http.NewServeMux()
mux.HandleFunc("GET /hello", helloHandler)
// 串联所有中间件(オニオンモデル:Recovery → Logging → CORS → Handler)
handler := Chain(mux,
RecoveryMiddleware,
LoggingMiddleware,
CORSMiddleware,
TimeoutMiddleware(5*time.Second),
)
log.Print(http.ListenAndServe(":8080", handler))
}
graph LR
Req[请求] --> R[Recovery]
R --> L[Logging]
L --> C[CORS]
C --> T[Timeout]
T --> H[Handler]
H -->|応答| T
T --> C
C --> L
L --> R
R --> Resp[応答]
(1) 4つの主要なミドルウェアコンポーネントの役割
| ミドルウェア | 役割 | 順序 |
|---|---|---|
| リカバリー | パニックをキャッチし、500を返す | 最外層 |
| ロギング | ロギングのリクエストおよびレスポンス時間 | レイヤー2 |
| CORS | クロスオリジンリクエストのヘッダーの処理 | レイヤー3 |
| タイムアウト | リクエスト・タイムアウト制御 | 最内層(ハンドラに近い) |
6. httputil.ReverseProxy リバースプロキシ
▶ サンプル:リバースプロキシ
パッケージ main
import (
"log"
"net/http"
"net/http/httputil"
"net/url"
)
func main() {
// 目标服务地址
target, _ := url.Parse("http://localhost:8081")
// リバースプロキシの作成
proxy := httputil.NewSingleHostReverseProxy(target)
// カスタマイズ Director(修改请求头)
proxy.Director = func(req *http.Request) {
req.URL.Scheme = target.Scheme
req.URL.Host = target.Host
req.URL.Path = target.Path + req.URL.Path
req.Header.Set("X-Forwarded-Host", req.Header.Get("Host"))
req.Header.Set("X-Real-IP", req.RemoteAddr)
}
// カスタムエラー処理
proxy.ErrorHandler = func(w http.ResponseWriter, r *http.Request, err エラー) {
log.Printf("プロキシエラー: %v", err)
http.Error(w, "Bad Gateway", http.StatusBadGateway)
}
mux := http.NewServeMux()
mux.HandleFunc("/api/", proxy.ServeHTTP)
log.Print("API 网关启动于 :8080")
log.Fatal(http.ListenAndServe(":8080", mux))
}
httputil.ReverseProxy は、ホストヘッダーの転送、X-Forwarded-For、レスポンスヘッダーの転送、接続プールの再利用など、プロキシに関するほとんどの処理を自動的に行います。必要なのは、リクエストを変更するための Director 関数を指定することだけです。
7. 完全な例:API Gateway + ミドルウェア・パイプライン
// api_gateway.go
package main
import (
"context"
"encoding/json"
"fmt"
"log"
"net/http"
"net/http/httputil"
"net/url"
"os"
"os/signal"
"strings"
"time"
)
// ---------- 構成 ----------
type Route struct {
Path string
Target *url.URL
}
type GatewayConfig struct {
Port string
Routes []Route
Middlewares []Middleware
}
// ---------- ミドルウェア ----------
type Middleware func(http.Handler) http.Handler
func Chain(handler http.Handler, middlewares ...Middleware) http.Handler {
for i := len(middlewares) - 1; i >= 0; i-- {
handler = middlewares[i](handler)
}
return handler
}
// ログ
func Logging(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))
})
}
// 復元
func Recovery(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
defer func() {
if err := recover(); err != nil {
log.Printf("[PANIC] %v", err)
http.Error(w, `{"error":"internal error"}`, http.StatusInternalServerError)
}
}()
next.ServeHTTP(w, r)
})
}
// CORS
func CORS(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 == http.MethodOptions {
w.WriteHeader(http.StatusNoContent)
return
}
next.ServeHTTP(w, r)
})
}
// トラフィック制限(トークンバケツ(簡易版))
type RateLimiter struct {
tokens chan struct{}
}
func NewRateLimiter(rate int) *RateLimiter {
rl := &RateLimiter{tokens: make(chan struct{}, rate)}
for i := 0; i < rate; i++ {
rl.tokens <- struct{}{}
}
// 每秒补充
go func() {
ticker := time.NewTicker(time.Second)
defer ticker.Stop()
for range ticker.C {
select {
case rl.tokens <- struct{}{}:
default:
}
}
}()
return rl
}
func (rl *RateLimiter) Middleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
select {
case <-rl.tokens:
next.ServeHTTP(w, r)
default:
http.Error(w, `{"error":"rate limit exceeded"}`, http.StatusTooManyRequests)
}
})
}
// ---------- 网关 ----------
type Gateway struct {
config GatewayConfig
proxy *httputil.ReverseProxy
}
func NewGateway(config GatewayConfig) *Gateway {
proxy := &httputil.ReverseProxy{
Director: func(req *http.Request) {
// リクエストパスに基づいてルーティングをマッチングする
for _, route := range config.Routes {
if strings.HasPrefix(req.URL.Path, route.Path) {
req.URL.Scheme = route.Target.Scheme
req.URL.Host = route.Target.Host
req.URL.Path = strings.TrimPrefix(req.URL.Path, route.Path)
req.Header.Set("X-Forwarded-Host", req.Header.Get("Host"))
return
}
}
},
ErrorHandler: func(w http.ResponseWriter, r *http.Request, err error) {
log.Printf("プロキシエラー: %v", err)
http.Error(w, `{"error":"bad gateway"}`, http.StatusBadGateway)
},
}
return &Gateway{config: config, proxy: proxy}
}
func (g *Gateway) Start() error {
routesJSON, _ := json.MarshalIndent(g.config.Routes, "", " ")
log.Printf("ルートの読み込みを開始します:\n%s\n", routesJSON)
// ルーティングの配布
mux := http.NewServeMux()
for _, route := range g.config.Routes {
pattern := route.Path
if !strings.HasSuffix(pattern, "/") {
pattern += "/"
}
mux.Handle(pattern, g.proxy)
}
// 健康診断
mux.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) {
json.NewEncoder(w).Encode(map[string]string{"status": "ok"})
})
// ミドルウェアチェーン
handler := Chain(mux, g.config.Middlewares...)
server := &http.Server{
Addr: ":" + g.config.Port,
Handler: handler,
}
// エレガントに閉じる
go func() {
sigCh := make(chan os.Signal, 1)
signal.Notify(sigCh, os.Interrupt)
<-sigCh
log.Println("サービスを停止中です...")
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
server.Shutdown(ctx)
}()
log.Printf("API 网关启动于 :%s", g.config.Port)
return server.ListenAndServe()
}
func main() {
userService, _ := url.Parse("http://localhost:8081")
orderService, _ := url.Parse("http://localhost:8082")
config := GatewayConfig{
Port: "8080",
Routes: []Route{
{Path: "/api/users", Target: userService},
{Path: "/api/orders", Target: orderService},
},
}
config.Middlewares = []Middleware{
Recovery,
Logging,
CORS,
}
gateway := NewGateway(config)
if err := gateway.Start(); err != nil && err != http.ErrServerClosed {
log.Fatal(err)
}
}
httputil.ReverseProxy はデフォルトで X-Forwarded-For ヘッダーを変更します。この動作を望まない場合は、Director でヘッダーを削除してください。また、ReverseProxy は WebSocket のアップグレードを自動的に処理しません。httputil.ReverseProxy の ServeHTTP メソッドは WebSocket をサポートしていないため、アップグレードリクエストは別途処理する必要があります。
❓ よくある質問
http.Get と http.Client のどちらを選べばよいですか?http.Get を使用してください。本番環境では、必ずカスタム設定の http.Client を使用してください。Timeout を設定してリクエストがハングアップするのを防ぎ、Transport を設定して接続プールやタイムアウトの詳細を制御してください。http.Get は http.DefaultClient を使用しますが、これにはデフォルトのタイムアウト設定がありません。IdleConnTimeoutを30~90秒に設定してください。Chain(h, A, B, C) → リクエストは A → B → C → ハンドラーを通過し、レスポンスは C → B → A を通過します。Director 関数を実装するだけで済みます。マイクロサービスアーキテクチャにおけるトラフィックのエントリポイントとして適しています。http.Client はスレッドセーフですか?http.Client は並行して使用できます(Get や Do を複数回呼び出しても、追加のロックは必要ありません)。ただし、Transport のフィールドは、最初の使用後は変更しないでください。ベストプラクティス:下流のサービスクラスタごとに1つのClientを作成し、リクエストごとに新しいClientを作成しないようにしてください。http.NewRequest を使用してリクエストを作成した後、次のようにヘッダーを設定してください:req.Header.Set("Key", "Value")。req.Header.Add("Key", "Value")—Addは上書きではなく追加されるため、使用しないでください。Auth、TraceID、Content-Typeなどの一般的なヘッダーは、すべてリクエストヘッダーとして渡されます。📖 まとめ
- 3種類のリクエスト:HTTP GET、POST、およびDO
- http.Client が応答しなくなるのを防ぐために、タイムアウトを設定する
- HTTP トランスポートの接続プール設定(MaxIdleConnsPerHost の値を増加させる)
- ミドルウェアパターン:リクエストが外側の層から内側の層へと伝播する「オニオンモデル」
- 4つの主要なミドルウェアコンポーネント:リカバリ → ロギング → CORS → タイムアウト
- httputil.ReverseProxy はリバースプロキシを実装しています
- API Gateway は、ミドルウェアチェーンとルーティングおよび分散処理を統合しています
📝 練習問題
-
基本演習(難易度:⭐):タイムアウトを5秒に設定したカスタム
http.ClientとMaxIdleConnsPerHost=20を作成してください。このClientを使用して https://httpbin.org/delay/3 へ GET リクエストを送信し、タイムアウトが期待通りに機能することを確認してください。 -
上級問題(難易度 ⭐⭐):リクエスト再試行ミドルウェア(RoundTripperレイヤー)を実装してください。要件:(1) GETリクエストを最大3回まで自動的に再試行すること;(2) 再試行間隔をそれぞれ100 ms、200 ms、400 msに設定すること(指数関数的バックオフ); (3) 5xxレスポンスのみをリトライし、4xxレスポンスはリトライしないこと; (4) 各リトライの試行をログに記録すること。
-
課題(難易度:⭐⭐⭐):ロードバランシング機能を備えたリバースプロキシを実装してください。要件:(1) 複数のバックエンドインスタンス(例:3つのユーザーサービスインスタンス)の登録に対応すること;(2) ラウンドロビンアルゴリズムを用いてリクエストを分散させること; (3) パッシブヘルスチェックを実行すること—3回連続して失敗したインスタンスを除外し、復旧したら再度追加すること;(4) 各リクエストがルーティングされたバックエンドのアドレスをログに記録すること;(5)
httputil.ReverseProxyを使用して実装すること。



