Web開発 - 解説
1. net/httpの内部アーキテクチャ
1.1 HTTPサーバーの動作原理
Goのnet/httpパッケージは、シンプルなAPIの背後に洗練されたアーキテクチャを持っています。
http.ListenAndServe(":8080", handler)
この1行で何が起こっているのか:
- TCPリスナーの起動
// 内部的には以下のような処理
ln, err := net.Listen("tcp", ":8080")
// ポート8080でTCP接続を待機
- Accept ループ
for {
conn, err := ln.Accept() // 新しい接続を待つ
go serve(conn, handler) // 各接続を別goroutineで処理
}
- リクエストの解析
GET /path HTTP/1.1)
- ヘッダーの読み取りと解析
- ボディの読み取り(必要に応じて)- ハンドラの呼び出し
handler.ServeHTTP(w, r)
1.2 コネクションの管理
Keep-Alive の処理
srv := &http.Server{
IdleTimeout: 60 * time.Second, // アイドル接続の保持時間
}
HTTPのKeep-Aliveにより、同じTCP接続で複数のリクエストを処理できます:
クライアント サーバー
| |
|--- TCP接続確立 ------------->|
| |
|--- GET /api/users ---------->|
|<-- 200 OK -------------------|
| |
|--- GET /api/posts ---------->| (同じTCP接続を再利用)
|<-- 200 OK -------------------|
| |
|--- 接続クローズ ------------->|
接続プール データベース接続では、接続プールが重要です:
db.SetMaxOpenConns(25) // 最大25の同時接続
db.SetMaxIdleConns(5) // アイドル状態で保持する接続数
db.SetConnMaxLifetime(5*time.Minute) // 接続の最大寿命
なぜ接続プールが必要か:
- TCP接続の確立はコストが高い(3-way handshake)
- データベース認証のオーバーヘッド
- リソースの効率的な利用
2. Handler, HandlerFunc, ServeMuxの違い
2.1 http.Handler インターフェース
type Handler interface {
ServeHTTP(ResponseWriter, *Request)
}
任意の型をハンドラにできる:
type MyHandler struct {
db *sql.DB
}
func (h *MyHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
// リクエストの処理
fmt.Fprintf(w, "Hello from MyHandler")
}
// 使用例
handler := &MyHandler{db: db}
http.ListenAndServe(":8080", handler)
2.2 http.HandlerFunc 型
関数をハンドラに変換するアダプター:
type HandlerFunc func(ResponseWriter, *Request)
func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) {
f(w, r) // 関数を呼び出すだけ
}
使用例:
func helloHandler(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Hello, World!")
}
// 関数をHandlerに変換
http.Handle("/", http.HandlerFunc(helloHandler))
// HandleFuncは内部的にHandlerFuncを使う
http.HandleFunc("/", helloHandler) // より簡潔
2.3 http.ServeMux - ルーター
複数のハンドラを管理するルーター:
mux := http.NewServeMux()
mux.HandleFunc("/users", usersHandler)
mux.HandleFunc("/posts", postsHandler)
パターンマッチングのルール(Go 1.22+):
mux.HandleFunc("GET /users/{id}", getUserHandler)
// ✓ GET /users/123 → マッチ
// ✗ POST /users/123 → マッチしない
// ✗ GET /users/123/abc → マッチしない
mux.HandleFunc("/static/", staticHandler)
// ✓ /static/css/style.css → マッチ(プレフィックス)
// ✓ /static/js/app.js → マッチ
優先順位:
- 完全一致(Exact match)
- 長いパターン優先
- 登録順
mux.HandleFunc("/api/users/me", meHandler) // 最優先
mux.HandleFunc("/api/users/{id}", userHandler) // 次
mux.HandleFunc("/api/", apiHandler) // 最後
3. Contextの正しい使い方
3.1 Contextの基本
Contextは以下の情報を運びます:
- キャンセルシグナル: 処理の中断要求
- タイムアウト: 処理の期限
- 値の伝播: リクエストスコープのデータ
func handler(w http.ResponseWriter, r *http.Request) {
// リクエストには既にContextが含まれている
ctx := r.Context()
// タイムアウト付きContextの作成
ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
defer cancel() // 必ず呼ぶ(リソースリーク防止)
// Contextを使った処理
result, err := fetchDataWithContext(ctx)
if err != nil {
if errors.Is(err, context.DeadlineExceeded) {
http.Error(w, "Request timeout", http.StatusGatewayTimeout)
return
}
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
json.NewEncoder(w).Encode(result)
}
3.2 データベースクエリでのContext
NG例:
rows, err := db.Query("SELECT * FROM users WHERE active = true")
// タイムアウトなし、キャンセル不可
OK例:
ctx, cancel := context.WithTimeout(r.Context(), 3*time.Second)
defer cancel()
rows, err := db.QueryContext(ctx, "SELECT * FROM users WHERE active = true")
// 3秒でタイムアウト、リクエストキャンセル時に自動終了
3.3 HTTPリクエストでのContext
外部APIを呼び出す場合:
func callExternalAPI(ctx context.Context, url string) (*Response, error) {
req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
if err != nil {
return nil, err
}
client := &http.Client{
Timeout: 10 * time.Second,
}
resp, err := client.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
// レスポンスの処理...
return parseResponse(resp)
}
Context伝播の例:
HTTPリクエスト
↓ (Context作成)
ハンドラ
↓ (Context伝播)
ビジネスロジック
↓ (Context伝播)
データベースクエリ
↓ (Context伝播)
外部APIコール
全ての層でContextが伝播され、ユーザーが接続を切断すると、全ての処理がキャンセルされます。
3.4 Contextに値を保存する
認証情報の伝播:
type contextKey string
const UserIDKey contextKey = "userID"
// ミドルウェアで値を設定
func AuthMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
userID := extractUserID(r) // トークンから抽出
ctx := context.WithValue(r.Context(), UserIDKey, userID)
next.ServeHTTP(w, r.WithContext(ctx))
})
}
// ハンドラで値を取得
func handler(w http.ResponseWriter, r *http.Request) {
userID := r.Context().Value(UserIDKey).(string)
// userIDを使った処理...
}
注意点:
- Contextの値はリクエストスコープのデータのみ
- 関数の引数で渡せるものはContextに入れない
- 型安全性のため、専用の型を定義する
4. パフォーマンス最適化
4.1 接続プールのチューニング
db.SetMaxOpenConns(25)
db.SetMaxIdleConns(5)
db.SetConnMaxLifetime(5 * time.Minute)
最適な値の決定:
- MaxIdleConns: MaxOpenConnsの20-30%
- ConnMaxLifetime: 5分程度
4.2 sync.Pool によるメモリ最適化
頻繁に生成・破棄されるオブジェクトのプール:
var bufferPool = sync.Pool{
New: func() interface{} {
return new(bytes.Buffer)
},
}
func handler(w http.ResponseWriter, r *http.Request) {
// Poolからバッファを取得
buf := bufferPool.Get().(*bytes.Buffer)
defer bufferPool.Put(buf) // 使用後にPoolに戻す
buf.Reset() // 内容をクリア
// バッファを使った処理
buf.WriteString("Hello, ")
buf.WriteString("World!")
w.Write(buf.Bytes())
}
効果:
- ガベージコレクションの負荷軽減
- メモリアロケーションの削減
- 高スループットアプリケーションで効果大
4.3 JSONエンコーディングの最適化
標準的な方法:
data := map[string]interface{}{
"name": "John",
"age": 30,
}
json.NewEncoder(w).Encode(data) // リフレクション使用
高速化(構造体を使用):
type User struct {
Name string `json:"name"`
Age int `json:"age"`
}
user := User{Name: "John", Age: 30}
json.NewEncoder(w).Encode(user) // リフレクション最小化
さらに高速化(easyjson等):
go get github.com/mailru/easyjson
easyjson -all user.go # コード生成
// 生成されたコードを使用
user.MarshalJSON() // 標準のjson.Marshalより5-10倍高速
4.4 レスポンスの圧縮
import "compress/gzip"
func gzipMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if !strings.Contains(r.Header.Get("Accept-Encoding"), "gzip") {
next.ServeHTTP(w, r)
return
}
gz := gzip.NewWriter(w)
defer gz.Close()
w.Header().Set("Content-Encoding", "gzip")
gzw := &gzipResponseWriter{Writer: gz, ResponseWriter: w}
next.ServeHTTP(gzw, r)
})
}
type gzipResponseWriter struct {
io.Writer
http.ResponseWriter
}
func (w *gzipResponseWriter) Write(b []byte) (int, error) {
return w.Writer.Write(b)
}
効果:
- 転送データ量: 70-90%削減(テキストベース)
- ネットワーク帯域の節約
- レスポンス時間の短縮(特にモバイル)
4.5 キャッシング戦略
インメモリキャッシュ:
import "github.com/patrickmn/go-cache"
var appCache = cache.New(5*time.Minute, 10*time.Minute)
func handler(w http.ResponseWriter, r *http.Request) {
key := r.URL.Path
// キャッシュチェック
if cached, found := appCache.Get(key); found {
w.Write(cached.([]byte))
return
}
// データ取得
data := fetchData()
// キャッシュに保存
appCache.Set(key, data, cache.DefaultExpiration)
w.Write(data)
}
HTTPキャッシュヘッダー:
func handler(w http.ResponseWriter, r *http.Request) {
// ブラウザキャッシュの指示
w.Header().Set("Cache-Control", "public, max-age=3600")
w.Header().Set("ETag", generateETag(data))
// 条件付きリクエストの処理
if r.Header.Get("If-None-Match") == etag {
w.WriteHeader(http.StatusNotModified)
return
}
// 通常のレスポンス
w.Write(data)
}
5. 実装のポイント
5.1 標準ライブラリの活用
Go 1.22から導入された新しいルーティング機能を使用しています:
mux.HandleFunc("GET /api/tasks/{id}", handler)
これにより、外部ライブラリなしで以下が可能です:
- HTTPメソッドベースのルーティング
- パスパラメータの抽出
- RESTful APIの設計
5.2 ミドルウェアパターン
Goのミドルウェアパターンは、http.Handlerをラップする関数です:
func Middleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// 前処理
next.ServeHTTP(w, r)
// 後処理
})
}
利点:
- 横断的関心事(ロギング、認証など)の分離
- 再利用可能なコンポーネント
- テストしやすい構造
5.3 構造化とパッケージ分割
- handlers/ → HTTPハンドラ
- models/ → ドメインモデル
- storage/ → データアクセス層
- middleware/ → 横断的関心事
設計判断:
- 責任の明確な分離
- 依存関係の方向が一方向(handlers → storage → models)
- テストしやすい構造
5.4 エラーハンドリング
複数のエラータイプを使い分けています:
// カスタムエラー
var ErrTaskNotFound = fmt.Errorf("task not found")
// 構造化されたエラー
type ValidationError struct {
Field string
Message string
}
エラーハンドリングの階層:
- バリデーションエラー → 400 Bad Request
- NotFoundエラー → 404 Not Found
- その他のエラー → 500 Internal Server Error
5.5 並行安全性
MemoryStoreはsync.RWMutexを使用して並行アクセスに対応:
func (s *MemoryStore) Get(id string) (*models.Task, error) {
s.mu.RLock() // 読み取りロック
defer s.mu.RUnlock()
// ...
}
func (s *MemoryStore) Create(task *models.Task) error {
s.mu.Lock() // 書き込みロック
defer s.mu.Unlock()
// ...
}
重要ポイント:
- 読み取りは複数同時可能(RLock)
- 書き込みは排他的(Lock)
deferで必ずアンロック
6. CS基礎との関連
6.1 ネットワークプログラミング
TCP/IPスタック:
Application Layer (HTTP)
↓
Transport Layer (TCP)
↓
Internet Layer (IP)
↓
Link Layer
HTTPリクエストの流れ:
- DNS解決: ドメイン名 → IPアドレス
- TCP接続: 3-way handshake
- HTTPリクエスト送信: アプリケーション層
- HTTPレスポンス受信: サーバーからの応答
- 接続クローズ: または Keep-Alive
- マップ(ハッシュテーブル)
6.2 データ構造
使用されるデータ構造:
tasks map[string]*models.Task
- O(1)の検索・挿入・削除
- 平均的な性能は優れているが、最悪ケースはO(n)- スライス(動的配列)
tasks := make([]*models.Task, 0)
- 順序付きコレクション
- 追加はO(1)(容量が十分な場合)- ツリー(B-tree)
6.3 並行処理モデル
ゴルーチンの実装:
- グリーンスレッド(ユーザー空間スレッド)
- M:N スケジューリング(M個のOSスレッドでN個のゴルーチンを実行)
- スタックサイズ: 2KB(OSスレッドは1-2MB)
チャネルの実装:
- ロックとセマフォを使った同期機構
- バッファ付き/なしチャネル
- select文による多重化
7. 発展的学習への道筋
7.1 Webフレームワークの選択肢
Gin - 最も人気のフレームワーク
import "github.com/gin-gonic/gin"
r := gin.Default()
r.GET("/users/:id", func(c *gin.Context) {
id := c.Param("id")
c.JSON(200, gin.H{"id": id})
})
特徴:
- 高速(httprouterベース)
- 豊富なミドルウェア
- JSONバリデーション組み込み
Echo - シンプルで拡張性が高い
import "github.com/labstack/echo/v4"
e := echo.New()
e.GET("/users/:id", func(c echo.Context) error {
return c.JSON(200, map[string]string{"id": c.Param("id")})
})
Fiber - Express.js風のAPI
import "github.com/gofiber/fiber/v2"
app := fiber.New()
app.Get("/users/:id", func(c *fiber.Ctx) error {
return c.JSON(fiber.Map{"id": c.Params("id")})
})
7.2 マイクロサービスアーキテクチャ
サービス間通信:
- REST API - 標準的なHTTP通信
- gRPC - 高速なRPC通信
service UserService {
rpc GetUser(UserRequest) returns (UserResponse);
}
- メッセージキュー - 非同期通信
サービスディスカバリ:
- Consul
- etcd
- Kubernetes Service
API Gateway:
クライアント
↓
API Gateway (認証、ルーティング、レート制限)
↓
┌──┴──┬──────┬──────┐
User Post Comment Payment
Service Service Service Service
7.3 クラウドネイティブ開発
Docker化:
FROM golang:1.22 AS builder
WORKDIR /app
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -o app
FROM alpine:latest
RUN apk --no-cache add ca-certificates
COPY --from=builder /app/app /app
CMD ["/app"]
Kubernetes デプロイ:
apiVersion: apps/v1
kind: Deployment
metadata:
name: api-server
spec:
replicas: 3
template:
spec:
containers:
- name: api
image: myapp:latest
ports:
- containerPort: 8080
Observability(可観測性):
- ロギング: 構造化ログ(JSON)
- メトリクス: Prometheus
- トレーシング: Jaeger, OpenTelemetry
import "go.opentelemetry.io/otel"
func handler(w http.ResponseWriter, r *http.Request) {
ctx, span := otel.Tracer("api").Start(r.Context(), "handleRequest")
defer span.End()
// 処理...
}
7.4 セキュリティのベストプラクティス
7.5 推奨学習パス
初級(1-2ヶ月):
- 標準ライブラリの習得
- 簡単なCRUD APIの作成
- ミドルウェアの理解
中級(3-6ヶ月):
- データベース統合(PostgreSQL)
- 認証・認可の実装
- テストの充実
- Dockerコンテナ化
上級(6ヶ月以上):
- マイクロサービス設計
- gRPC実装
- Kubernetes運用
- 分散トレーシング
- パフォーマンスチューニング
- 実践プロジェクト: 自分のアイデアでAPIを構築
- OSSへの貢献: 人気フレームワークのコード読解
- 本番運用: クラウドへのデプロイと監視
- コミュニティ: Go Conference、meetupへの参加
- 継続学習: 新しいパターン、ツールの探索
次のステップ
さらに学びを深めるために: