はじめに
この記事は、TIS アドベントカレンダー2025 の3日目の記事です。
全3回にわたって以下のAIチャットで見るようなリアルタイムレスポンスの実装方法の解説をします。
- Go言語でAmazon BedrockのInvokeMoelWithResponseStreamを使ったサーバーサイドの実装
- Go言語で、Ginフレームワークを使ったServer-Sent-Eventsの応答の実装
- Reactで、Server-Sent-Eventsを使ったリアルタイムレスポンスの表示の実装
本記事は、2回目のGinを使ったServer-Sent-Eventsのサーバー側の実装です。
どんなことをしたいか?(再掲)
以下の画像のような、AIを使ったチャットアプリケーションで、リアルタイムにレスポンスを表示する実装をしたい場面がありました。
要素技術的には、
- リアルタイムレスポンスを返すAPIとしては、Amazon BedrockのInvokeModelWithResponseStreamを使い、
- Server-Sent-Eventsという技術を利用すればできそうでしたのでサンプルを作りました。
今回はサーバー側のAPIサーバーの実装の紹介です。
Ginとは?
Go言語で書かれたWeb Application サーバーのフレームワークです。
高速、軽量が売りです。その他の同様のライブラリには、Echo、Martiniなどがあります。
Server-Sent-Eventsとは?
Webサーバーからリアルタイムにデータを送信するための技術です。
- サーバーからのPUSH通知に利用できる
- 接続を張り続け、切断されても自動接続される
- 標準ではHTTPリクエストのGETのみサポート
という特徴があります。
GETのみがクライアントのEventSourceというAPIでサポートされていますが、POSTで使うケースもままあるのではないでしょうか? 今回の記事では、POSTとして実装し、3回目の記事では、POSTのケースでのServer-sent-eventsに対応しています。
サーバー側の実装
APIサーバーは、以下の流れで実装しています。
(1) routerを作成する(gin.Default()で作成できる)
(2) corsの設定をする
(3) APIのルーティングとハンドラを登録する
package main
import (
"gin-sse-sample/chat"
"log"
"os"
"path/filepath"
"strings"
"time"
"github.com/gin-contrib/cors"
"github.com/gin-gonic/gin"
"github.com/joho/godotenv"
)
func main() {
// .envファイルの読み込み
err := godotenv.Load(".env")
if err != nil {
log.Fatalf(".envファイルの読み込みに失敗しました。 %v", err)
}
router := gin.Default()
// corsの設定
allowOrigins := strings.Split(os.Getenv("API_SERVER_CORS_ALLOW_ORIGINS"), ",")
router.Use(cors.New(
cors.Config{
AllowOrigins: allowOrigins,
AllowMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"},
AllowHeaders: []string{"Origin", "Content-Type", "Accept"},
ExposeHeaders: []string{"Content-Length"},
AllowCredentials: true,
MaxAge: 12 * time.Hour,
},
))
group := router.Group("/chat")
chat.RegisterRoutes(group)
router.GET("/healthcheck", func(c *gin.Context) {
c.JSON(200, gin.H{
"message": "ok",
})
})
router.Run("localhost:4000")
}
URLの /chat/stream に対応したハンドラは以下の実装で登録しています。
(1) router.POST("stream", handlePostStream)でPOSTに対応するハンドラを登録
(2) channelを作り、前回の記事のInvokeModelWithResponseStreamの出力をチャンネル経由で送信
(3) チャンネルから受信した情報を、context.Stream(func(w io.Writer) bool) を使ってServer-Sent-Eventsとして送出(context.SSEvent(label, message)を利用)
package chat
import (
"context"
"gin-sse-sample/bedrock"
"io"
"github.com/gin-gonic/gin"
)
func RegisterRoutes(router *gin.RouterGroup) {
router.POST("/stream", handlePostStream)
}
type PromptRequest struct {
Prompt string `json:"prompt" binding:"required"`
}
func handlePostStream(c *gin.Context) {
var request PromptRequest
if err := c.ShouldBind(&request); err != nil {
c.JSON(400, gin.H{"error": err.Error()})
return
}
client, err := bedrock.New()
if err != nil {
c.JSON(500, gin.H{"error": err.Error()})
return
}
channel := make(chan bedrock.ResponseData)
go client.InvokeModelWithResponseStream(
context.Background(),
request.Prompt,
channel,
)
c.Stream(func(w io.Writer) bool {
if msg, ok := <-channel; ok {
c.SSEvent("data", msg)
return true
}
return false
})
}
シンプルに書くなら、以下の箇所だけでServer-Sent-Eventsを実装できます。
// 何らかのチャンネルを用意する(ここではstring型としている)
channel := make(chan string)
// 外部APIを呼び出して、channelにデータを逐次投入
go externalAPI(channel)
c.Stream(func(w io.Writer) bool {
if msg, ok := <-channel; ok {
// channelから取り出したデータをSSEventでクライアントに返却
c.SSEvent("data", msg)
return true
}
return false
})
さいごに
context.Streamとcontext.SSEventのみで実装できます。簡単でしたね。
次回は、Server-Sent-Eventsのクライアント(React)編です。
