Skip to content

后端工程与基础设施

一、SSE 与 WebSocket 实时通信

普通 HTTP 通常是前端发起一次请求,后端完成处理后返回一次响应。但在 AI 对话和实时事件推送中,后端需要持续向前端发送数据,因此会用到 SSE 或 WebSocket。

SSE 流式响应

text/event-stream 表示使用 SSE 流式响应。它仍然基于 HTTP,但不会等待全部内容生成完成后再一次性返回,而是生成一部分就发送一部分。

text
前端发起请求
→ 后端保持当前 HTTP 连接
→ AI 生成一部分内容
→ 立即发送给前端
→ 持续发送直到回复结束

所以聊天接口会把 ctx 一起传给 Service 层,让 Service 可以通过当前 HTTP 连接持续返回 AI 回复。

WebSocket 长连接

WebSocket 会先由前端通过 HTTP 发起连接,再由后端通过 Upgrade 将普通 HTTP 连接升级成 WebSocket 长连接。

text
前端发起 HTTP 请求
→ 后端校验用户和会话
→ Upgrade 升级连接
→ 得到 WebSocket 连接对象 conn
→ Service 层管理后续通信

conn 是升级成功后得到的 WebSocket 连接对象,通常是 *websocket.Conn。它代表前端和后端之间已经建立好的通信通道,后续可以通过它读取消息、发送消息和关闭连接。

SSE 和 WebSocket 的主要区别:

  • SSE 主要是服务端持续向前端发送数据,适合 AI 回复流式输出
  • WebSocket 支持前端和后端双向通信,适合实时事件推送和状态同步

二、文件上传与处理

multipart/form-data 是 HTTP 请求中常用的文件上传格式。它不仅可以传文件,也可以同时传普通文本参数。

当前语音转文字接口中主要包含:

  • audio:用户上传的音频文件
  • language:音频对应的语言参数
go
file, err := ctx.FormFile("audio")

ctx.FormFile("audio") 会从表单数据中取出名字叫 audio 的文件。得到的 file 里面包含文件名、文件大小,以及打开文件的方法等信息。

在把文件传进 Service 层之前,需要先检查文件是否为空、是否超过最大大小。这样可以防止无效文件或者过大文件继续消耗服务器资源。

go
if file.Size <= 0 || file.Size > chat.MaxSpeechAudioBytes {
	ctx.BadRequest()
	return nil, errors2.ErrorInvalidParams
}

ctx.BadRequest() 表示当前请求参数不符合要求,一般对应 HTTP 状态码 400

文件打开、读取和关闭

前面拿到的 file 主要保存上传文件的信息,file.Open() 才是真正打开文件,并得到可以读取内容的对象 src

go
src, err := file.Open()
if err != nil {
	return nil, errors2.ErrorInternal
}
defer src.Close()

defer src.Close() 表示在当前函数结束前关闭文件。即使中途因为错误提前 return,也会执行关闭操作,避免文件资源一直被占用。

go
audioData, err := io.ReadAll(src)

io.ReadAll(src) 会把文件中原本就有的全部字节读取到内存,结果保存在 audioData 这个 []byte 变量里。它不会重新生成数据,也不会改变音频格式,只是把原有文件字节读取出来。

完整处理流程:

text
获取上传文件
→ 校验文件是否合法
→ 打开文件
→ 设置函数结束前关闭文件
→ 读取成 []byte
→ 传给 Service 层继续处理

三、默认参数与数据整理

有些请求参数不是必须由前端传入,后端可以为它设置默认值。

go
language := ctx.DefaultPostForm("language", "zh-CN")

这段代码表示:

  • 前端传了 language,就使用前端传入的值
  • 前端没有传,就默认使用 zh-CN

文件、表单和 JSON 最开始都属于原始请求数据。API 层会先完成解析、校验和默认值补充,再把 Service 层真正需要的数据整理进结构体。

go
&chat.TranscribeSpeechRequest{
	Filename: file.Filename,
	Audio:    audioData,
	Language: language,
}

这里是创建一个请求结构体,给字段赋值,再直接取得它的地址传给 Service 层。结构体可以明确每个字段的名字和类型,让不同层之间的数据流转更清楚。

四、配置与配置读取

配置就是决定程序怎样运行的一组可调整参数。很多运行时选择不应该直接写死在业务代码中,而应该通过配置控制。

常见配置包括:

  • 使用哪个模型或者第三方服务
  • API 地址、API Key 和 Token
  • 数据库地址
  • 服务端口
  • 超时时间
  • 语音识别使用 Google 还是火山引擎

配置文件中的内容原本是文本。项目启动时会读取这些内容,再解析到 Go 结构体变量中。这个保存在内存里、可以被代码读取的结构体变量,可以理解成配置对象。

常见的配置来源有:

  • YAML 配置文件
  • 环境变量
  • Docker 或服务器运行环境

YAML 是一种常见的配置文件格式,常见文件名是 config.yaml。环境变量则由操作系统、服务器或者 Docker 提供,API Key、Token 这类敏感信息经常放在环境变量中。

go
global.GVA_CONFIG.BusinessAi.SpeechProvider

这段代码是在全局配置中逐层读取语音识别服务商:

  • global:项目中保存全局数据的包
  • GVA_CONFIG:项目读取并保存好的总配置变量
  • BusinessAi:总配置中和 AI 业务有关的部分
  • SpeechProvider:决定使用哪个语音识别服务商的字段
go
provider := strings.TrimSpace(
	global.GVA_CONFIG.BusinessAi.SpeechProvider,
)

strings.TrimSpace 会删除字符串前后的空格。例如把 " google " 处理成 "google",避免配置中的多余空格影响判断。

五、配置驱动的外部服务分流

provider 是从服务器配置中读取出来的服务商名称。Google 和火山两套调用逻辑已经提前写好,程序运行时根据配置决定实际调用哪一套。

text
读取 SpeechProvider
→ 判断 provider 的值
→ google:调用 Google 语音识别
→ 其他值:调用火山引擎语音识别

这里三个参数的作用不同:

  • provider:决定调用哪个语音识别服务商
  • language:告诉识别服务这段音频是什么语言
  • audioData:真正需要识别的音频字节数据

当前代码不是根据音频语言自动选择服务商,而是由服务器全局配置决定。因此一般是所有用户共同使用服务器选定的语音识别服务商。

这种方式可以理解成配置驱动的业务分流:

多套业务实现已经提前写好,程序运行时读取配置,再决定调用哪个函数。切换服务商时主要修改配置,不需要重写主要业务流程。

如果以后需要让每个用户自己选择服务商,就需要继续增加:

  • 请求参数或请求结构体中的服务商字段
  • API 层对用户选择的接收和校验
  • Service 层根据用户选择调用不同服务
  • 用户偏好在数据库中的保存和读取

六、API 层在这类场景中的工作

普通 API 层主要负责解析参数、补充可信身份、调用 Service 和返回结果。在文件上传和外部服务场景中,API 层还需要完成更多参数准备工作:

  • 校验上传文件是否合法
  • 打开、读取和关闭文件
  • 将文件内容整理成字节数据
  • 为可选参数补充默认值
  • 读取服务器配置
  • 根据配置选择不同的 Service 函数
  • 构造对应 Service 层需要的请求结构体

只要参数、文件打开或读取过程中出现错误,就立即 return,防止错误数据继续传进后续业务逻辑。