使用 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 测试和前端实时渲染
相关推荐
冰暮流星15 分钟前
javascript逻辑运算符
开发语言·javascript·ecmascript
flysh0516 分钟前
如何利用 C# 内置的 Action 和 Func 委托
开发语言·c#
码农小韩37 分钟前
基于Linux的C++学习——动态数组容器vector
linux·c语言·开发语言·数据结构·c++·单片机·学习
木风小助理38 分钟前
`mapfile`命令详解:Bash中高效的文本至数组转换工具
开发语言·chrome·bash
yyy(十一月限定版)1 小时前
初始matlab
开发语言·matlab
LawrenceLan1 小时前
Flutter 零基础入门(九):构造函数、命名构造函数与 this 关键字
开发语言·flutter·dart
listhi5201 小时前
基于MATLAB的支持向量机(SVM)医学图像分割方法
开发语言·matlab
韩师傅1 小时前
前端开发消亡史:AI也无法掩盖没有设计创造力的真相
前端·人工智能·后端
hui函数1 小时前
如何解决 pip install 编译报错 g++: command not found(缺少 C++ 编译器)问题
开发语言·c++·pip
Tisfy1 小时前
网站访问耗时优化 - 从数十秒到几百毫秒的“零成本”优化过程
服务器·开发语言·性能优化·php·网站·建站