使用 Go 实现 SSE 流式推送 + 打字机效果(模拟 Coze Chat)

使用 Go 实现 SSE 流式推送 + 打字机效果(模拟 Coze Chat)

在开发实时聊天、AI 助手或者协作应用时,我们经常需要 SSE(Server-Sent Events) 实现服务端向前端持续推送数据。本文将分享一个 Go SSE 打字机式输出实现,并附上上游模拟示例、curl 测试和前端实时渲染示例。


功能特点

  1. 使用 SSE 推送消息流,前端无需轮询。
  2. 对消息进行 逐字符打字机式输出,模拟 AI 打字效果。
  3. 支持 上游 SSE 模拟,方便本地测试。
  4. 可轻松扩展为真实 AI 聊天接口的代理服务。

技术栈

  • Go 1.21+
  • CloudWeGo Hertz 作为 HTTP 框架
  • resty 用于上游 SSE 请求
  • SSE 流式推送(使用 hertz-contrib/sse

完整示例代码

go 复制代码
package main

import (
	"bufio"
	"context"
	"encoding/json"
	"fmt"
	"math/rand"
	"strings"
	"time"

	"github.com/cloudwego/hertz/pkg/app"
	"github.com/cloudwego/hertz/pkg/app/server"
	"github.com/cloudwego/hertz/pkg/common/hlog"
	"github.com/hertz-contrib/sse"
	"resty.dev/v3"
)

func main() {
	h := server.Default(server.WithHostPorts(":8380"))

	h.POST("/v3/chat", CozeParseThenTypeWriter)
	h.GET("/upstream", MockUpstreamSSE)

	hlog.Info("🚀 Coze SSE parse + typewriter proxy running at :8380")
	h.Spin()
}

// 核心逻辑:解析 conversation.message.delta → 打字机式输出
func CozeParseThenTypeWriter(ctx context.Context, c *app.RequestContext) {
	// SSE Header
	c.SetStatusCode(200)
	h := c.Response.Header
	h.Set("Content-Type", "text/event-stream; charset=utf-8")
	h.Set("Cache-Control", "no-cache, no-store, must-revalidate")
	h.Set("Pragma", "no-cache")
	h.Set("Connection", "keep-alive")
	h.Set("X-Accel-Buffering", "no")

	stream := sse.NewStream(c)

	// Resty 上游请求
	client := resty.New().SetTimeout(0)
	resp, err := client.R().
		SetContext(ctx).
		SetDoNotParseResponse(true).
		SetHeader("Accept", "text/event-stream").
		Get("http://localhost:8380/upstream")

	if err != nil || resp.RawResponse == nil || resp.RawResponse.Body == nil {
		hlog.Error("upstream connect failed")
		return
	}
	defer resp.RawResponse.Body.Close()

	// 打字机准备
	r := rand.New(rand.NewSource(time.Now().UnixNano()))
	scanner := bufio.NewScanner(resp.RawResponse.Body)
	var currentEvent string

	// SSE 解析循环
	for scanner.Scan() {
		select {
		case <-ctx.Done():
			hlog.Warn("client disconnected")
			return
		default:
		}

		line := scanner.Text()

		if line == "" {
			currentEvent = ""
			continue
		}

		if strings.HasPrefix(line, "event:") {
			currentEvent = strings.TrimSpace(strings.TrimPrefix(line, "event:"))
			continue
		}

		if strings.HasPrefix(line, "data:") {
			payload := strings.TrimSpace(strings.TrimPrefix(line, "data:"))

			if payload == "[DONE]" {
				stream.Publish(&sse.Event{Data: []byte("[DONE]")})
				return
			}

			if currentEvent == "conversation.message.delta" {
				var d struct {
					Content string `json:"content"`
				}
				if err := json.Unmarshal([]byte(payload), &d); err == nil {
					typeWriter(stream, r, d.Content)
				}
			}

			if currentEvent == "conversation.message.completed" {
				stream.Publish(&sse.Event{
					Event: "conversation.message.completed",
					Data:  []byte(`{"status":"completed"}`),
				})
			}
		}
	}
}

// 打字机逐字符输出
func typeWriter(stream *sse.Stream, r *rand.Rand, text string) {
	for i, ch := range text {
		time.Sleep(getSleepDuration(r, ch))

		data := map[string]any{
			"id":         fmt.Sprintf("char_%d_%d", i, time.Now().UnixNano()%100000),
			"role":       "assistant",
			"type":       "answer",
			"content":    string(ch),
			"created_at": time.Now().UnixMilli(),
		}

		b, _ := json.Marshal(data)

		_ = stream.Publish(&sse.Event{
			Event: "conversation.message.delta",
			ID:    fmt.Sprintf("char_%d", i),
			Data:  b,
		})
	}
}

// 延迟策略
func getSleepDuration(r *rand.Rand, ch rune) time.Duration {
	switch {
	case ch == '\n' || ch == '。' || ch == '!' || ch == '?':
		return time.Duration(300+r.Intn(200)) * time.Millisecond
	case ch == '、' || ch == ' ' || ch == '-' || ch == ':' || ch == ',':
		return time.Duration(150+r.Intn(100)) * time.Millisecond
	case ch == '#' || ch == '*' || ch == '>':
		return time.Duration(200+r.Intn(150)) * time.Millisecond
	default:
		return time.Duration(60+r.Intn(60)) * time.Millisecond
	}
}

// 上游 Mock(模拟 Coze SSE)
func MockUpstreamSSE(ctx context.Context, c *app.RequestContext) {
	c.SetStatusCode(200)
	c.Header("Content-Type", "text/event-stream")
	c.Header("Cache-Control", "no-cache")
	c.Header("Connection", "keep-alive")
	c.Header("X-Accel-Buffering", "no")
	c.Flush()

	send := func(event, data string) {
		c.Write([]byte(
			"event: " + event + "\n" +
				"data: " + data + "\n\n",
		))
		c.Flush()
	}

	deltas := []string{"你", "好", ",", "这", "是", " Coze", " SSE"}

	for _, ch := range deltas {
		send("conversation.message.delta", `{"content":"`+ch+`"}`)
		time.Sleep(80 * time.Millisecond)
	}

	send("conversation.message.completed", `{}`)
	c.Write([]byte("data: [DONE]\n\n"))
	c.Flush()
}

使用方法

  1. 启动服务
bash 复制代码
go run main.go
  1. 访问接口
  • 上游 SSE 测试(浏览器可直接访问):
    http://localhost:8380/upstream

  • 打字机代理接口(POST):
    http://localhost:8380/v3/chat


使用 curl 测试 SSE

bash 复制代码
# 测试上游 SSE
curl -N http://localhost:8380/upstream

# 测试打字机代理 SSE
curl -N -X POST http://localhost:8380/v3/chat

参数说明:

  • -N / --no-buffer:禁用输出缓存,实时显示流式数据。
  • -X POST:因为代理接口是 POST。

运行后,你会在终端看到类似打字机逐字符输出:

复制代码
event: conversation.message.delta
data: {"id":"char_0_12345","role":"assistant","type":"answer","content":"你","created_at":1700000000000}

event: conversation.message.delta
data: {"id":"char_1_67890","role":"assistant","type":"answer","content":"好","created_at":1700000000050}
...
event: conversation.message.completed
data: {"status":"completed"}

data: [DONE]

前端实时渲染打字机效果示例

在前端,可以使用 EventSource 监听 SSE 并动态显示内容:

html 复制代码
<div id="chat"></div>

<script>
const chatDiv = document.getElementById("chat");
const evtSource = new EventSource("http://localhost:8380/v3/chat");

evtSource.addEventListener("conversation.message.delta", e => {
    const data = JSON.parse(e.data);
    chatDiv.innerHTML += data.content;
});

evtSource.addEventListener("conversation.message.completed", e => {
    console.log("消息完成");
});

evtSource.onopen = () => console.log("连接已打开");
evtSource.onerror = () => console.log("连接错误或关闭");
</script>

效果:消息逐字符显示,模拟 AI 打字机输出。


核心解析

  1. SSE Header 设置
go 复制代码
h.Set("Content-Type", "text/event-stream; charset=utf-8")
h.Set("Cache-Control", "no-cache, no-store, must-revalidate")
h.Set("Connection", "keep-alive")
h.Set("X-Accel-Buffering", "no")

保证浏览器或代理实时接收流式数据。

  1. 打字机效果

根据字符类型不同设置不同延迟:

go 复制代码
case ch == '\n' || ch == '。' || ch == '!' || ch == '?':
    return time.Duration(300+r.Intn(200)) * time.Millisecond
  1. 上游 SSE 模拟

方便本地测试,无需真实 AI 接口即可验证前端打字机效果。


总结

通过本文示例,你可以快速实现:

  • Go SSE 服务端代理
  • AI 聊天消息打字机式输出
  • 上游 SSE 模拟
  • curl 测试和前端实时渲染
相关推荐
JIngJaneIL2 小时前
基于springboot + vue古城景区管理系统(源码+数据库+文档)
java·开发语言·前端·数据库·vue.js·spring boot·后端
敲敲了个代码2 小时前
隐式类型转换:哈基米 == 猫 ? true :false
开发语言·前端·javascript·学习·面试·web
小信啊啊2 小时前
Go语言切片slice
开发语言·后端·golang
阿华hhh2 小时前
Linux系统编程(标准io)
linux·开发语言·c++
南_山无梅落3 小时前
9.Python3集合(set)增删改查和推导式
java·开发语言
sg_knight3 小时前
拥抱未来:ECMAScript Modules (ESM) 深度解析
开发语言·前端·javascript·vue·ecmascript·web·esm
程序喵大人3 小时前
推荐个 C++ 练习平台
开发语言·c++·工具推荐
阿里嘎多学长3 小时前
2025-12-16 GitHub 热点项目精选
开发语言·程序员·github·代码托管
乂爻yiyao4 小时前
Java LTS版本重要升级特性对照表
java·开发语言