Appearance
后端工程与基础设施
一、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,防止错误数据继续传进后续业务逻辑。