Ir al contenido principal
友田 陽大
Go & Echo in production
Go
Echo
リアルタイム
アーキテクチャ設計
セキュリティ
可観測性

Echo リアルタイム実装:WebSocket と SSE の使い分け・本番設計(切断検知・認証・スケール)

Go Echo(v5)でリアルタイム機能を本番品質に実装するガイド。SSE(text/event-stream + http.NewResponseController でフラッシュ)と WebSocket(gorilla/websocket でアップグレード)の実装、両者の使い分け、切断検知・WriteTimeout 無効化・Origin 検証・WebSocket 認証・複数インスタンスでのスケール(Redis Pub/Sub)までを実コードで解説します。

Published
Reading time
10 min read
Author
友田 陽大
Compartir

「通知をリアルタイムで出したい」「チャットを作りたい」「処理の進捗をライブで見せたい」——これらは要件としては一行ですが、実装の選択肢(SSE か WebSocket か)と本番の罠(切断検知・タイムアウト・認証・スケール)を外すと、繋がらない・落ちる・乗っ取られる機能になります。

この記事は、Go Echo 本番運用ガイドのリアルタイム編です。まず使い分けを明確にし、Echo v5 での SSEWebSocket の実装、そして本番で必ずぶつかる4つの罠の対策を解説します。

この記事のルール:Echo の API は 公式ドキュメント(v5・2026年6月時点) に基づきます。WebSocket は標準 net/http 互換のため github.com/gorilla/websocket(公式 Cookbook が採用)を用います。coder/websocket 等の代替も同様に使えます。アーキテクチャ選定の全体像はWebSocket/SSE のアーキテクチャ決定も参照してください。


0. 最初に決める:SSE か WebSocket か

実装に入る前に、方向性で選びます。ここを間違えると、不要に複雑な実装を背負います。

観点SSE(Server-Sent Events)WebSocket
通信方向サーバー → クライアント(一方向)双方向
プロトコル普通の HTTP(GET)専用(HTTP からアップグレード)
自動再接続ブラウザが標準で行う自前実装が必要
プロキシ/FW 通過HTTP なので通りやすい環境によっては弾かれる
実装コスト低いやや高い
向く用途通知・進捗・株価・ライブフィードチャット・ゲーム・協調編集

判断の目安サーバーからの一方向通知(通知バッジ、ジョブ進捗、ダッシュボード更新)なら、迷わず SSE。HTTP のままで実装が軽く、再接続もブラウザ任せにできます。クライアントからも頻繁に送る双方向(チャット、リアルタイム協調)なら WebSocket。「とりあえず WebSocket」は、多くの通知系では過剰です(YAGNI)。


1. SSE:一方向ストリーミングの本命

Echo v5 では c.Response()http.ResponseWriter を返すので、SSE は標準的な HTTP ストリーミングとして素直に書けます。ポイントは text/event-stream ヘッダと、http.NewResponseController(w).Flush() による即時送出、そして c.Request().Context().Done() での切断検知です。

func (h *Handler) Stream(c *echo.Context) error {
	w := c.Response()
	w.Header().Set("Content-Type", "text/event-stream")
	w.Header().Set("Cache-Control", "no-cache")
	w.Header().Set("Connection", "keep-alive")

	rc := http.NewResponseController(w) // Go 1.20+ の標準。Flush を型アサーション無しで呼べる
	ticker := time.NewTicker(time.Second)
	defer ticker.Stop()

	for {
		select {
		case <-c.Request().Context().Done():
			// クライアント切断・サーバー停止 → ループを抜ける(goroutine リーク防止)
			return nil
		case ev := <-h.events: // 配信したいイベント源(チャネル)
			// SSE の書式:data: <payload>\n\n
			if _, err := fmt.Fprintf(w, "event: %s\ndata: %s\n\n", ev.Name, ev.JSON); err != nil {
				return err
			}
			if err := rc.Flush(); err != nil { // バッファを今すぐ送る
				return err
			}
		case <-ticker.C:
			// ハートビート(コメント行)。プロキシのアイドル切断を防ぐ
			fmt.Fprint(w, ": ping\n\n")
			_ = rc.Flush()
		}
	}
}

クライアントは標準の EventSource で受けるだけです(自動再接続つき)。

const es = new EventSource("/api/v1/stream");
es.addEventListener("progress", (e) => update(JSON.parse(e.data)));

1.1 SSE 最大の罠:WriteTimeout に殺される

デプロイ編で設定した WriteTimeout は、長時間接続の SSE/WebSocket を途中で切断します。リアルタイム経路では、サーバーの WriteTimeout を **0(無効)**にします。Echo v5 では StartConfig.BeforeServeFunc で設定します。

sc := echo.StartConfig{
	Address: ":8080",
	BeforeServeFunc: func(s *http.Server) error {
		s.WriteTimeout = 0      // 長時間ストリーミングを切らない
		s.ReadHeaderTimeout = 5 * time.Second // ヘッダ系の保護は残す
		return nil
	},
}

設計の分離:通常 API は WriteTimeout を効かせたい、SSE は効かせたくない——要件が矛盾します。理想は、リアルタイム用のサーバー(ポート)を通常 API と分けること。難しければ、上記のように WriteTimeout=0 にしつつ、SSE ハンドラ側でidle タイムアウトを自前管理します。


2. WebSocket:双方向通信の実装

WebSocket は HTTP 接続をアップグレードして確立します。net/http 互換なので、c.Response()c.Request()gorilla/websocketUpgrader に渡すだけです。

import "github.com/gorilla/websocket"

var upgrader = websocket.Upgrader{
	ReadBufferSize:  1024,
	WriteBufferSize: 1024,
	CheckOrigin:     checkOrigin, // ← セキュリティ上、必ず実装(第4章)
}

func (h *Handler) WS(c *echo.Context) error {
	conn, err := upgrader.Upgrade(c.Response(), c.Request(), nil)
	if err != nil {
		return err // アップグレード失敗(既にレスポンス書込済みのことが多い)
	}
	defer conn.Close()

	for {
		// 読み取り(クライアント → サーバー)
		mt, msg, err := conn.ReadMessage()
		if err != nil {
			if websocket.IsUnexpectedCloseError(err, websocket.CloseGoingAway) {
				c.Logger().Error("ws read error", "error", err)
			}
			break // 切断 → ループを抜ける
		}
		// 書き込み(サーバー → クライアント):ここではエコー
		if err := conn.WriteMessage(mt, msg); err != nil {
			break
		}
	}
	return nil
}

2.1 読み書きを別 goroutine に分け、ping/pong で死活監視する

本番では、読み取りと書き込みを別 goroutineに分け、ping/pong で切断を検知します(gorilla の定石)。1接続 = 2 goroutine(reader/writer)+ 送信チャネル、という構成にします。

// writer:送信専用 goroutine。送信チャネルと定期 ping を一手に握る
func (cl *Client) writePump() {
	pingTicker := time.NewTicker(30 * time.Second)
	defer func() { pingTicker.Stop(); cl.conn.Close() }()
	for {
		select {
		case msg, ok := <-cl.send:
			_ = cl.conn.SetWriteDeadline(time.Now().Add(10 * time.Second))
			if !ok {
				_ = cl.conn.WriteMessage(websocket.CloseMessage, nil)
				return
			}
			if err := cl.conn.WriteMessage(websocket.TextMessage, msg); err != nil {
				return
			}
		case <-pingTicker.C:
			_ = cl.conn.SetWriteDeadline(time.Now().Add(10 * time.Second))
			if err := cl.conn.WriteMessage(websocket.PingMessage, nil); err != nil {
				return // pong が返らなければ相手は死んでいる
			}
		}
	}
}

SetReadDeadline + SetPongHandler で、pong が来るたびに読み取り期限を延ばします。これでゾンビ接続(TCP は生きているが相手は応答しない)を回収できます。


3. 本番の罠:切断検知とリソースリーク

リアルタイムの事故の大半は、接続が切れたのに goroutine が残ることです。1接続あたり数 goroutine が漏れると、接続を繰り返すうちにメモリと goroutine が枯渇します。

対策
SSE で client 切断に気づかないc.Request().Context().Done() を select し、必ずループを抜ける
WS のゾンビ接続ping/pong + ReadDeadline で死活監視し、無応答を切る
goroutine リーク1接続のライフサイクル(reader/writer)を必ず defer conn.Close() で閉じる
バックプレッシャ送信チャネルをバッファ付きにし、詰まったら遅い client を切る(全体を巻き込まない)
グレースフル停止SIGTERM 時に全接続へ close を送ってから終了

「遅い1クライアントが全体のレイテンシを悪化させる」のを防ぐには、送信が詰まった接続を切る判断が要ります。リアルタイム系は「全員に届けようと粘る」より「届かない相手は切る」方が全体の健全性を保てます。


4. セキュリティ:Origin 検証と WebSocket 認証

4.1 CheckOrigin:CSWSH を防ぐ

WebSocket には同一オリジンポリシーが効きませんCheckOrigin を実装しないと、悪意あるサイトがユーザーの Cookie を使って WebSocket を確立する **CSWSH(Cross-Site WebSocket Hijacking)**が可能になります。必ず Origin を許可リストで検証します。

var allowedOrigins = map[string]bool{"https://app.example.com": true}

func checkOrigin(r *http.Request) bool {
	return allowedOrigins[r.Header.Get("Origin")] // 既定の「常に true」は危険
}

gorilla/websocketCheckOrigin は**デフォルトが「同一ホストのみ許可」**ですが、リバースプロキシ背後では正しく機能しないことがあるため、明示的に許可リストを実装するのが安全です。

4.2 WebSocket の認証:ブラウザはヘッダを付けられない

ブラウザの WebSocket API はカスタムヘッダ(Authorization)を付けられません。そのためJWT 認証をそのまま流用できません。現実的な選択肢:

  • Cookie(httpOnly):同一サイトなら Cookie は自動送出される。CSRF/Origin 対策と併用。最も素直。
  • 接続後の最初のメッセージでトークンを送る:アップグレード自体は通し、最初のフレームで JWT を検証、ダメなら即 close。
  • Sec-WebSocket-Protocol(subprotocol)にトークンを載せる:仕様準拠だが扱いがやや特殊。

アップグレード前に Echo のミドルウェアで Cookie 認証を通し、CheckOrigin で Origin を縛る、の二段が実装も堅牢性もバランスが良い構成です。


5. スケール:複数インスタンスでは「インメモリ Hub」だけでは届かない

ここが本番の最重要ポイントです。WebSocket/SSE の接続は、それを確立した特定のインスタンスに固定されます。インスタンスが2台以上あると、インスタンス A に繋がっている user へ、インスタンス B のイベントが届きません

[インスタンスA] ── user1, user2  ←┐
                                   │ A 内のイベントは A の接続にしか届かない
[インスタンスB] ── user3, user4  ←┘ B のイベントは B の接続にしか届かない

解決策は、メッセージブローカ(Redis Pub/Sub 等)越しに配信することです。各インスタンスはブローカを購読し、受け取ったメッセージを自分が抱える接続に配ります。

イベント発生 → [Redis Pub/Sub に publish]
                    ↓ 全インスタンスが購読
        [A]→A の接続へ   [B]→B の接続へ   全 user に届く
  • 単一インスタンス(小規模):インメモリの Hub(接続レジストリ)で十分。KISS。
  • 複数インスタンス(本番):Redis Pub/Sub・NATS・Kafka 等でインスタンス間を橋渡し。あるいは、リアルタイム配信自体をマネージドサービス(API Gateway WebSocket / Ably / Pusher)に逃がす判断もあります。

この「接続のアフィニティ」と「ブローカ越し配信」の設計判断は、リアルタイムアーキテクチャの決定ガイドで詳しく扱っています。可観測性(接続数・配信レイテンシのメトリクス)はOpenTelemetry 連携で計測します。


まとめ:リアルタイムを本番品質にする7原則

  1. 方向で選ぶ。一方向通知は SSE、双方向は WebSocket。多くは SSE で足りる(YAGNI)。
  2. SSE は text/event-stream + http.NewResponseController(w).Flush()、切断は ctx.Done() で検知。
  3. 長時間接続は WriteTimeout=0BeforeServeFunc)。可能ならリアルタイム用にサーバーを分ける。
  4. WebSocket は reader/writer を分け、ping/pong で死活監視defer conn.Close() でリークを防ぐ。
  5. CheckOrigin を必ず実装して CSWSH を防ぐ。
  6. WS 認証は Cookie か接続後トークン(ブラウザはヘッダを付けられない)。
  7. 複数インスタンスは Redis Pub/Sub 等で橋渡し。インメモリ Hub だけでは全体に届かない。

リアルタイムは「繋ぐ」ことより「切る・守る・スケールさせる」ことの方が難しい領域です。まず SSE で要件が満たせないかを問い、必要な双方向性だけを WebSocket で足す——この順番が、保守しやすいリアルタイム機能の近道です。全体像はGo Echo 本番運用ガイドへ。

友田

友田 陽大

Creador de un producto galardonado con el Premio del Ministro (METI). Con TypeScript + Python + AWS entrego SaaS, digitalización industrial e IA generativa (RAG) lista para producción, de principio a fin: desde los requisitos hasta la infraestructura y la operación, yo solo.

I can take on the implementation from this article as an engagement

I build Go / Echo backends, from design to production

API design and migration to Echo v5, clean architecture (Controller/UseCase/Repository + DI), middleware and security, centralized error handling, graceful shutdown, and testing/CI. With experience building a clean-architecture backend in Go/Echo + google/wire, I implement APIs that don't fall over, are traceable, and are easy to change.

Disponible tanto para proyectos cerrados como para asesoría técnica. Empieza con una consulta gratuita de 30 minutos.

最短ルート:カレンダーから直接予約

相談内容が固まっている方は、フォーム送信よりその場で日程を確定する方がスムーズです。下記から空き時間をお選びください。

  • 30分のオンライン無料相談
  • Google Meet / Zoom / Microsoft Teams
  • NDA 商談前締結可・無理な営業はいたしません
無料相談の空き枠を予約する

También te puede interesar