使用 Go 调用 OpenAI API 实战:构建支持流式输出的聊天应用

“—\ntitle: \”使用 Go 调用 OpenAI API 实战:构建支持流式输出的聊天应用\”\ndescription: \”本文详细讲解如何使用 Go 语言调用 OpenAI GPT API,包含完整的代码示例、非流式和流式两种调用方式,以及错误处理和生产环境注意事项。\”\nslug: go-openai-streaming-chat\ntags:\n- Go\n- OpenAI API\n- 流式响应\n- AI 编程\n- SSE\ncategories:\n- 技术教程\n- Go 编程\n- AI 应用\n—\n\n# 使用 Go 调用 OpenAI API 实战:构建支持流式输出的聊天应用\n\n## 背景介绍\n\n去年开始,我把大量时间花在 AI 应用开发上。作为一个 Go 爱好者,我一直在琢磨怎么把这个新潮玩艺儿和熟悉的语言结合起来。最开始踩了不少坑——要么被 API 调用阻塞,要么流式输出不会处理,折腾好久才搞明白。\n\n今天把踩过的坑整理一下,写个完整的实战教程。如果你也需要流式响应(一个字一个字蹦出来的效果),这篇文章应该能帮上忙。\n\n## 问题描述\n\n调用 OpenAI API 看起来简单,实际上有几个地方容易卡住:\n\n**1. 请求格式问题**\nchat completions 接口使用特定的 JSON 格式,需要指定 model 和 messages 数组。消息角色写错了,接口直接返回 400 错误。我第一次调用时就因为把 \”system\” 拼成 \”systemMessage\” 折腾了半小时。\n\n**2. 流式响应处理**\nGPT 输出的一大段文本,如果等产品全部生成完再返回,用户得等到天荒地老。OpenAI 支持 Server-Sent Events(SSE),可以一边生成一边返回,但这涉及到 SSE 流的解析问题。Go 标准库没有原生支持,得自己动手。\n\n**3. 超时和重试**\n网络请求难免会遇到超时、限流(429 错误)。生产级别的代码必须处理好这些边界情况。\n\n## 详细步骤\n\n### 1. 环境配置\n\n首先你需要有个 OpenAI API key,去 platform.openai.com 注册账号然后创建 API key。把 key 保存到环境变量里:\n\n“`bash\nexport OPENAI_API_KEY=sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\n“`\n\n如果你用的是代理或者自定义 endpoint(比如调用 Claude 或者其他兼容 OpenAI API 的服务),可以额外设置:\n\n“`bash\nexport OPENAI_BASE_URL=https://api.custom.com/v1\n“`\n\nGo 代码里用 os.Getenv 读取这些值就行。\n\n### 2. 项目初始化\n\n创建一个新的 Go 项目:\n\n“`bash\nmkdir ai-chat-demo && cd ai-chat-demo\ngo mod init ai-chat-demo\n“`\n\n直接用标准库加少量辅助代码就够了。\n\n### 3. 代码实现\n\n创建 main.go 文件。首先是基础的请求结构:\n\n“`go\npackage main\n\nimport (\n \”bufio\”\n \”bytes\”\n \”encoding/json\”\n \”fmt\”\n \”io\”\n \”net/http\”\n \”os\”\n \”strings\”\n \”time\”\n)\n\n// 请求消息结构\ntype Message struct {\n Role string `json:\”role\”`\n Content string `json:\”content\”`\n}\n\n// ChatCompletionsRequest 定义发送给 OpenAI 的请求体\ntype ChatCompletionsRequest struct {\n Model string `json:\”model\”`\n Messages []Message `json:\”messages\”`\n Stream bool `json:\”stream,omitempty\”`\n MaxTokens int `json:\”max_tokens,omitempty\”`\n Temperature float64 `json:\”temperature,omitempty\”`\n}\n\n// Choice 响应中的选项结构\ntype Choice struct {\n Index int `json:\”index\”`\n Message Message `json:\”message\”`\n FinishReason string `json:\”finish_reason\”`\n}\n\n// UsageToken 使用的 token 数量\ntype UsageToken struct {\n PromptTokens int `json:\”prompt_tokens\”`\n CompletionTokens int `json:\”completion_tokens\”`\n TotalTokens int `json:\”total_tokens\”`\n}\n\n// ChatCompletionsResponse 完整的响应结构\ntype ChatCompletionsResponse struct {\n Id string `json:\”id\”`\n Object string `json:\”object\”`\n Created int64 `json:\”created\”`\n Model string `json:\”model\”`\n Choices []Choice `json:\”choices\”`\n Usage UsageToken `json:\”usage\”`\n}\n“`\n\n基础的调用函数(非流式):\n\n“`go\n// OpenAIClient OpenAI API 客户端\ntype OpenAIClient struct {\n apiKey string\n baseURL string\n httpClient *http.Client\n}\n\n// NewOpenAIClient 创建新的客户端\nfunc NewOpenAIClient(apiKey, baseURL string) *OpenAIClient {\n if baseURL == \”\” {\n baseURL = \”https://api.openai.com/v1\”\n }\n \n return &OpenAIClient{\n apiKey: apiKey,\n baseURL: baseURL,\n httpClient: &http.Client{\n Timeout: 120 * time.Second,\n },\n }\n}\n\n// Chat 完成一次聊天请求(非流式)\nfunc (c *OpenAIClient) Chat(messages []Message) (*ChatCompletionsResponse, error) {\n url := c.baseURL + \”/chat/completions\”\n \n // 构建请求体\n reqBody := ChatCompletionsRequest{\n Model: \”gpt-4o\”,\n Messages: messages,\n MaxTokens: 2048,\n Temperature: 0.7,\n }\n \n jsonBody, err := json.Marshal(reqBody)\n if err != nil {\n return nil, fmt.Errorf(\”failed to marshal request: %w\”, err)\n }\n \n // 创建请求\n req, err := http.NewRequest(\”POST\”, url, bytes.NewBuffer(jsonBody))\n if err != nil {\n return nil, fmt.Errorf(\”failed to create request: %w\”, err)\n }\n \n // 设置请求头\n req.Header.Set(\”Content-Type\”, \”application/json\”)\n req.Header.Set(\”Authorization\”, \”Bearer \”+c.apiKey)\n \n // 发送请求\n resp, err := c.httpClient.Do(req)\n if err != nil {\n return nil, fmt.Errorf(\”failed to send request: %w\”, err)\n }\n defer resp.Body.Close()\n \n // 读取响应体\n body, err := io.ReadAll(resp.Body)\n if err != nil {\n return nil, fmt.Errorf(\”failed to read response: %w\”, err)\n }\n \n // 检查 HTTP 状态码\n if resp.StatusCode != http.StatusOK {\n return nil, fmt.Errorf(\”API request failed with status %d: %s\”, resp.StatusCode, string(body))\n }\n \n // 解析响应\n var result ChatCompletionsResponse\n if err := json.Unmarshal(body, &result); err != nil {\n return nil, fmt.Errorf(\”failed to unmarshal response: %w, body: %s\”, err, string(body))\n }\n \n return &result, nil\n}\n“`\n\n需要逐行读取,然后提取 content 字段:\n\n“`go\n// StreamChunk 流式响应的单个块\ntype StreamChunk struct {\n Id string `json:\”id\”`\n Object string `json:\”object\”`\n Created int64 `json:\”created\”`\n Model string `json:\”model\”`\n Choices []struct {\n Index int `json:\”index\”`\n Delta struct {\n Role string `json:\”role,omitempty\”`\n Content string `json:\”content,omitempty\”`\n } `json:\”delta\”`\n FinishReason string `json:\”finish_reason,omitempty\”`\n } `json:\”choices\”`\n}\n\n// StreamChat 流式聊天,返回一个 channel 供消费\nfunc (c *OpenAIClient) StreamChat(messages []Message) (<-chan string, error) {\n url := c.baseURL + \"/chat/completions\"\n \n reqBody := ChatCompletionsRequest{\n Model: \"gpt-4o\",\n Messages: messages,\n Stream: true, // 关键:开启流���\n MaxTokens: 2048,\n }\n \n jsonBody, err := json.Marshal(reqBody)\n if err != nil {\n return nil, fmt.Errorf(\"failed to marshal request: %w\", err)\n }\n \n req, err := http.NewRequest(\"POST\", url, bytes.NewBuffer(jsonBody))\n if err != nil {\n return nil, fmt.Errorf(\"failed to create request: %w\", err)\n }\n \n req.Header.Set(\"Content-Type\", \"application/json\")\n req.Header.Set(\"Authorization\", \"Bearer \"+c.apiKey)\n req.Header.Set(\"Accept\", \"text/event-stream\")\n \n resp, err := c.httpClient.Do(req)\n if err != nil {\n return nil, fmt.Errorf(\"failed to send request: %w\", err)\n }\n \n if resp.StatusCode != http.Client {\n body, _ := io.ReadAll(resp.Body)\n resp.Body.Close()\n return nil, fmt.Errorf(\"API request failed with status %d: %s\", resp.StatusCode, string(body))\n }\n \n // 创建 channel 用于传递内容\n contentChan := make(chan string, 100)\n \n // 启动后台 goroutine 读取流\n go func() {\n reader := bufio.NewReader(resp.Body)\n \n for {\n line, err := reader.ReadString('\\n')\n if err != nil {\n break\n }\n \n line = strings.TrimSpace(line)\n \n // 跳过空行和注释行\n if line == \"\" || strings.HasPrefix(line, \":\") {\n continue\n }\n \n // 解析 SSE 行\n if strings.HasPrefix(line, \"data:\") {\n data := strings.TrimPrefix(line, \"data:\")\n data = strings.TrimSpace(data)\n \n // 检查结束标记\n if data == \"[DONE]\" {\n close(contentChan)\n resp.Body.Close()\n return\n }\n \n // 解析 JSON\n var chunk StreamChunk\n if err := json.Unmarshal([]byte(data), &chunk); err != nil {\n continue\n }\n \n // 提取内容\n if len(chunk.Choices) > 0 && chunk.Choices[0].Delta.Content != \”\” {\n contentChan <- chunk.Choices[0].Delta.Content\n }\n }\n }\n \n close(contentChan)\n resp.Body.Close()\n }()\n \n return contentChan, nil\n}\n```\n\n最后是主函数,把这些串起来:\n\n```go\nfunc main() {\n apiKey := os.Getenv(\"OPENAI_API_KEY\")\n baseURL := os.Getenv(\"OPENAI_BASE_URL\")\n \n if apiKey == \"\" {\n fmt.Println(\"请设置 OPENAI_API_KEY 环境变量\")\n fmt.Println(\"export OPENAI_API_KEY=your-api-key\")\n os.Exit(1)\n }\n \n client := NewOpenAIClient(apiKey, baseURL)\n \n // 构建对话历史\n messages := []Message{\n {Role: \"system\", Content: \"你是一个有帮助的助手,用简洁清晰的语言回答问题。\"},\n {Role: \"user\", Content: \"用 Go 怎么并发处理多个任务?请给出例子。\"},\n }\n \n fmt.Println(\"=== 非流式调用 ===\")\n fmt.Println(\"提问:\", messages[1].Content)\n fmt.Println(\"\\n回答:\")\n \n resp, err := client.Chat(messages)\n if err != nil {\n fmt.Printf(\"Error: %v\\n\", err)\n return\n }\n \n if len(resp.Choices) > 0 {\n fmt.Println(resp.Choices[0].Message.Content)\n }\n fmt.Printf(\”\\n使用 Token 数: %d\\n\”, resp.Usage.TotalTokens)\n \n fmt.Println(\”\\n=== 流式调用 ===\”)\n fmt.Println(\”提问:\”, messages[1].Content)\n fmt.Print(\”\\n回答: \”)\n \n streamChan, err := client.StreamChat(messages)\n if err != nil {\n fmt.Printf(\”Error: %v\\n\”, err)\n return\n }\n \n for content := range streamChan {\n fmt.Print(content)\n }\n fmt.Println()\n}\n“`\n\n## 运行结果\n\n运行一下看看效果:\n\n“`bash\ngo run main.go\n“`\n\n非流式调用的输出是一次性返回的:\n\n“`\n=== 非流式调用 ===\n提问: 用 Go 怎么并发处理多个任务?请给出例子。\n\n回答:\n在 Go 中,可以使用 goroutine 和 channel 来实现并发处理多个任务。\n\n最常用的方式是使用 sync.WaitGroup:\n\n“`go\nvar wg sync.WaitGroup\ntasks := []string{\”task1\”, \”task2\”, \”task3\”}\n\nfor _, task := range tasks {\n wg.Add(1)\n go func(t string) {\n defer wg.Done()\n // 处理任务\n fmt.Println(\”处理:\”, t)\n }(task)\n}\n\nwg.Wait()\n“`\n\n或者使用 worker pool 模式限制并发数…\n\n使用 Token 数: 423\n“`\n\n流式调用则会一个字一个字地蹦出来,体验完全不一样:\n\n“`\n=== 流式调用 ===\n提问: 用 Go 怎么并发处理多个任务?请给出例子。\n\n回答: 在 Go 中,你可以用 goroutine 加 channel 来处理并发任务。比如用 sync.WaitGroup…\n\n(内容逐渐显示)\n“`\n\n可以看到流式输出的优势——不需要等产品全部生���完就能看到内容,体验接近和真人对话。\n\n## 总结\n\n好了,整合一下今天学到的东西:\n\n使用 Go 调用 OpenAI API 的核心点:\n\n**请求构建**\n- 用正确的 JSON 格式指定 model、messages\n- messages 数组中的角色必须是 \”system\”、\”user\”、\”assistant\”\n\n**流式处理**\n- 将 Stream 设置为 true\n- 解析 SSE 格式,逐行读取 \”data:\” 开头的行\n- 检测 \”[DONE]\” 标记来判断何时结束\n\n**异常处理**\n- 设置合理的超时时间\n- 处理限流 429 错误(可以加指数退避重试)\n- 检查 HTTP 状态码\n\n这套代码可以直接扩展,换成 Claude 或者本地部署的模型只需要改 baseURL 就够了。如果要做真正的产品,再加上对话历史的存储和 token 计数限流等功能,就可以上线了。\n\n有问题欢迎评论区交流。”

暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇