はじめに
チャットツールみたいに「何かあったらすぐ画面に反映させたい」っていうリアルタイムな仕組み、いざ作ろうとすると意外と悩みますよね。
定番なのはWebSocketですが、実は「サーバーから通知を送るだけ」ならSSE(Server-Sent Events)がめちゃくちゃ手軽で便利です。普通のHTTPの仕組みの上で動くので、インフラ周りでハマることが少ないのも嬉しいポイント。
ということで、今回から数回に分けて、GoとReactを組み合わせてSSEをフル活用する仕組みを自作していこうと思います。
こんなことを学習していきます
- SSEとWebSocket、どっちをいつ使うべきか?
- Goで「接続中のユーザー全員に一斉通知」を送るハブの作り方
- ReactでSSEをスマートに受け取る方法
- 送信はREST API、受信はSSEっていう「いいとこ取り」な設計
まずはその第一歩として、Step 1では全ての土台になる「バックエンドの配信基盤」をGoでサクッと作っていきます。
SSE実装の全体ロードマップ
- 【本記事】Step 1:Goで最小限のSSEサーバーを作る
- Step 2:Reactでシグナルの受信を実装する
- Step 3:GoでHub(配信所)を設計し、一斉送信を可能にする
- Step 4:API送信と連携し、実用的なチャット基盤を完成させる
今回のゴール:配信サーバー基盤の構築(PoC)
成果物
まずは難しいことは抜きにして、「SSEの仕組み」だけを動かしてみます。
ブラウザやターミナルからアクセスすると、接続しっぱなしの状態で、サーバーから2秒おきに「Ping!」というメッセージが降ってくるようなAPIを作ります。
プロジェクトのディレクトリ構成
適当なディレクトリ(例: sse-project)を作って、こんな感じの構成にします。
sse-project/
├── docker-compose.yml
└── backend/
├── Dockerfile
└── main.go
(※go.mod は後でコマンドで作ります)
1. インフラの準備(Docker環境)
まずは docker-compose.yml を用意します。
# docker-compose.yml
version: "3.9"
services:
backend:
build: ./backend
ports:
- "8080:8080"
volumes:
- ./backend:/app
次に、backend ディレクトリの中に Dockerfile を作ります。
Go 1.26をベースに、保存するたびに自動で再起動してくれる air を入れちゃいます。
# backend/Dockerfile
FROM golang:1.26-alpine
WORKDIR /app
# ホットリロードツール「Air」をインストール
RUN go install [github.com/air-verse/air@latest](https://github.com/air-verse/air@latest)
# 起動時は air を使う
CMD ["air"]
2. GoでSSEハンドラを書く
backend/main.go を書いていきます。
Go標準の http.Flusher を使うのがコツです。これのおかげで、レスポンスを終わらせずにデータを「後出し」し続けることができます。
// backend/main.go
package main
import (
"fmt"
"log"
"net/http"
"time"
)
func main() {
// SSEのエンドポイント
http.HandleFunc("/events", sseHandler)
fmt.Println("SSE Server is running on http://localhost:8080")
if err := http.ListenAndServe(":8080", nil); err != nil {
log.Fatal(err)
}
}
func sseHandler(w http.ResponseWriter, r *http.Request) {
// 1. SSEに必要なヘッダーをセット
w.Header().Set("Content-Type", "text/event-stream")
w.Header().Set("Cache-Control", "no-cache")
w.Header().Set("Connection", "keep-alive")
// React(別ポート)から繋げるようにCORSを許可
w.Header().Set("Access-Control-Allow-Origin", "*")
// 2. http.Flusherを使えるようにする
flusher, ok := w.(http.Flusher)
if !ok {
http.Error(w, "Streaming unsupported!", http.StatusInternalServerError)
return
}
fmt.Println("Client connected!")
// 2秒おきに通知を送るためのタイマー
ticker := time.NewTicker(2 * time.Second)
defer ticker.Stop()
// 3. クライアントが切れるまでループ!
for {
select {
case <-r.Context().Done():
// ブラウザを閉じたりした時の切断検知
fmt.Println("Client disconnected")
return
case t := <-ticker.C:
// SSEの決まり文句 "data: <中身>\n\n" で送る
msg := fmt.Sprintf(`{"time": "%s", "message": "Ping!"}`, t.Format("15:04:05"))
fmt.Fprintf(w, "data: %s\n\n", msg)
// 溜め込まずにすぐ送り出す
flusher.Flush()
}
}
}
3. 起動して確認してみる
手順 1: Goモジュールの初期化
cd backend
go mod init sse-project
cd ..
手順 2: コンテナ起動
docker compose up --build
これで air が立ち上がって、コードの監視が始まります。
手順 3: ターミナルで確認
別のターミナルから curl を叩いてみます。-N をつけるのを忘れずに。
curl -N http://localhost:8080/events
こんな感じで、2秒ごとにデータが降ってきたら成功です!
おわりに
今回はサーバー側から一方的に送りつける基盤ができました。
次回のStep 2では、この降ってきたデータをReactでキャッチして、画面にリアルタイムで表示させていきます。
