Go 语言怎么接入 OpenAI 兼容 API 教程
Go 接大模型有两条路:用官方风格 SDK(go-openai)图省事,还是用标准库 net/http 图可控。这篇给两套完整可跑的代码、流式输出的 channel 写法、超时与重试的正确位置,以及一个会踩的坑:默认的 HTTP Client 没有超时。
Go 接大模型接口,纠结的点通常不是"能不能调通",而是"用哪个客户端、超时怎么设、并发怎么控"。网上大量示例直接 http.DefaultClient 一把梭,本地跑得通,上线遇到上游慢响应就把 goroutine 和连接池一起拖住。
这篇给两条路:一是 sashabaranov/go-openai 这套事实标准 SDK,二是只用标准库手写请求。前者快,后者清楚每一步发生了什么。两套都以 OpenAI 兼容协议为准,base_url 换成 https://api.zaimod.com/v1 即可,模型名从模型列表里挑。
先说结论
- 只要文本对话、JSON 输出、函数调用这三类需求,用
go-openai就够,别自己拼 JSON - 一定要自己构造
http.Client并设Timeout,Go 的默认客户端没有超时,上游卡住时请求会一直挂着 - 流式输出用 channel 往上传,不要让 HTTP handler 直接读 SDK 的流
- 并发控制放在 worker 池,不要用
go func()无脑起,容易把配额瞬间打完 - 重试只对可恢复状态码(429、5xx)做,并且必须带指数退避,否则会把限流放大成雪崩
方案一:go-openai SDK
依赖只有一个包:
go get github.com/sashabaranov/go-openai
下面是一个完整可运行的 main.go,包含自定义 base URL、超时控制和一次普通对话:
package main
import (
"context"
"fmt"
"net/http"
"os"
"time"
openai "github.com/sashabaranov/go-openai"
)
func main() {
cfg := openai.DefaultConfig(os.Getenv("ZAIMOD_API_KEY"))
cfg.BaseURL = "https://api.zaimod.com/v1"
// 关键:SDK 内部用的是你给的 Client,超时在这里设
cfg.HTTPClient = &http.Client{Timeout: 60 * time.Second}
client := openai.NewClientWithConfig(cfg)
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
defer cancel()
resp, err := client.CreateChatCompletion(ctx, openai.ChatCompletionRequest{
Model: "gpt-4o-mini",
Messages: []openai.ChatCompletionMessage{
{Role: openai.ChatMessageRoleSystem, Content: "你是一个简洁的技术助手。"},
{Role: openai.ChatMessageRoleUser, Content: "用一句话解释什么是 SSE。"},
},
Temperature: 0.3,
MaxTokens: 200,
})
if err != nil {
// 生产环境这里要区分错误类型再决定是否重试
fmt.Fprintf(os.Stderr, "请求失败: %v\n", err)
os.Exit(1)
}
fmt.Println(resp.Choices[0].Message.Content)
fmt.Printf("tokens: 输入 %d / 输出 %d\n",
resp.Usage.PromptTokens, resp.Usage.CompletionTokens)
}
注意代码块里少了 net/http 的 import,实际项目里补上即可。
几个容易忽略的点:
DefaultConfig的BaseURL默认指向官方域名,必须显式覆盖,否则请求会打到 OpenAI 而不是你的中转地址MaxTokens不填时由服务端决定上限,长输出任务里不设会拿到被截断的结果却不报错Usage字段是账单对账的唯一依据,建议每个请求都落库
方案二:只用标准库
不想引入第三方依赖时,标准库完全够用。核心是把"请求"和"响应"写成两个结构体,字段只声明你要用的部分:
type chatReq struct {
Model string `json:"model"`
Messages []message `json:"messages"`
Stream bool `json:"stream"`
}
type message struct {
Role string `json:"role"`
Content string `json:"content"`
}
func chat(ctx context.Context, msgs []message) (string, error) {
body, _ := json.Marshal(chatReq{Model: "gpt-4o-mini", Messages: msgs})
req, err := http.NewRequestWithContext(ctx, http.MethodPost,
"https://api.zaimod.com/v1/chat/completions", bytes.NewReader(body))
if err != nil {
return "", err
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", "Bearer "+os.Getenv("ZAIMOD_API_KEY"))
resp, err := httpClient.Do(req)
if err != nil {
return "", err
}
defer resp.Body.Close()
raw, _ := io.ReadAll(io.LimitReader(resp.Body, 4<<20))
if resp.StatusCode != http.StatusOK {
return "", fmt.Errorf("status=%d body=%s", resp.StatusCode, raw)
}
var out struct {
Choices []struct {
Message struct{ Content string } `json:"message"`
} `json:"choices"`
}
if err := json.Unmarshal(raw, &out); err != nil {
return "", err
}
if len(out.Choices) == 0 {
return "", fmt.Errorf("空 choices")
}
return out.Choices[0].Message.Content, nil
}
io.LimitReader 那行不是多余的:上游返回异常时可能吐出很大的错误页,限制读取长度可以避免内存被一次读爆。
流式输出:用 channel 隔开两层
流式输出的正确结构是「读流 → 发 channel → handler 消费」,SDK 那一层不要出现在 handler 里:
func stream(ctx context.Context, msgs []message) (<-chan string, <-chan error) {
out := make(chan string, 32)
errCh := make(chan error, 1)
go func() {
defer close(out)
defer close(errCh)
s, err := client.CreateChatCompletionStream(ctx, openai.ChatCompletionRequest{
Model: "gpt-4o-mini", Messages: toSDK(msgs), Stream: true,
})
if err != nil {
errCh <- err
return
}
defer s.Close()
for {
r, err := s.Recv()
if errors.Is(err, io.EOF) {
return
}
if err != nil {
errCh <- err
return
}
for _, c := range r.Choices {
select {
case out <- c.Delta.Content:
case <-ctx.Done():
return
}
}
}
}()
return out, errCh
}
channel 给 32 的缓冲,是为了在下游(比如 WebSocket 写客户端)短暂拥堵时不会立刻阻塞读流。缓冲给太大反而会掩盖下游问题。
超时该分三层设
Go 项目里只有一个全局超时是不够的,分层才可控:
| 层级 | 控制手段 | 建议值 | 作用 |
|---|---|---|---|
| 单次请求 | context.WithTimeout | 30–60 秒 | 防止一次调用无限挂起 |
| 首包等待 | Transport.ResponseHeaderTimeout | 20 秒 | 上游没起来时快速失败 |
| 连接建立 | Transport.DialContext 超时 | 5 秒 | 网络问题快速切走 |
Transport 的 MaxIdleConnsPerHost 建议调到 50 以上。默认值是 2,高并发下会反复建连,延迟里一半是 TCP 握手。
并发与重试
批量任务用 worker 池,worker 数量按上游配额定,不要按 CPU 核数定——瓶颈在网络不在本地:
sem := make(chan struct{}, 8) // 同时最多 8 个在飞
var wg sync.WaitGroup
for _, item := range items {
wg.Add(1)
go func(it Item) {
defer wg.Done()
sem <- struct{}{}
defer func() { <-sem }()
res, err := chatWithRetry(ctx, it)
if err != nil {
log.Printf("item=%s err=%v", it.ID, err)
return
}
save(it.ID, res)
}(item)
}
wg.Wait()
重试函数只处理 429 和 5xx,退避用 2^n 加抖动,最多 3 次:
func chatWithRetry(ctx context.Context, m []message) (string, error) {
var lastErr error
for i := 0; i < 3; i++ {
s, err := chat(ctx, m)
if err == nil {
return s, nil
}
lastErr = err
if !retryable(err) {
break
}
backoff := time.Duration(1<<i) * time.Second
jitter := time.Duration(rand.Intn(500)) * time.Millisecond
select {
case <-time.After(backoff + jitter):
case <-ctx.Done():
return "", ctx.Err()
}
}
return "", lastErr
}
retryable 一定要判断状态码而不是"err != nil 就重试"。参数错误、模型名写错这类 4xx 重试一百次也不会成功,只是在浪费额度。429 的详细处理方式见限流专题。
请求体字段速查
| 字段 | 类型 | 什么时候必须填 | 备注 |
|---|---|---|---|
model | string | 总是 | 名称以模型列表为准 |
messages | array | 总是 | 第一条可以是 system |
max_tokens | int | 长输出任务 | 不填则服务端决定上限 |
temperature | float | 需要稳定输出时 | 抽取任务建议 0–0.3 |
stream | bool | 前端实时显示 | 开启后响应是 SSE |
response_format | object | 需要 JSON | 设 {"type":"json_object"} |
response_format 只是"让它倾向输出 JSON",不等于 schema 校验。真正的保障仍然是服务端解析一次,解析失败再处理。协议细节见 OpenAI 兼容协议解析。
FAQ
go-openai 和官方 SDK 有什么区别?
OpenAI 官方目前没有维护 Go SDK,社区里 sashabaranov/go-openai 是事实标准。它走 OpenAI 兼容协议,因此换 BaseURL 后所有兼容实现都能用,包括 Claude、Gemini、DeepSeek 的兼容端点。
调用返回 200 但 content 为空是什么原因?
常见三种:max_tokens 设得太小导致输出被截断、模型触发了拒答、用了思考型模型但把思考内容当成了答案字段。先打印完整响应体的 finish_reason,能直接定位。
为什么 Go 里并发 100 个请求反而比并发 10 个慢?
多数是 MaxIdleConnsPerHost 太小导致连接复用失效,或者上游配额被打满后开始限流。先看耗时分布里有没有大量 429,再调连接池参数。
能不能直接用 http.DefaultClient?
本地调试可以,线上不行。它没有超时,Transport 也是共享的,任何一个卡住的请求都会影响其他调用方。
延伸阅读
- Node.js 接入 OpenAI 兼容 API:同为服务端语言,可对照超时与重试写法
- Java Spring Boot 接入示例:企业项目里的客户端封装思路
- SSE 流式输出从前端到后端:流式链路的完整说明
- 快速开始文档:五分钟拿到第一个可用的 Key
Go 项目里最省事的做法是只写一次客户端封装,Key 从环境变量读。注册后创建 API Key,把上面代码里的 BaseURL 和 api_key 换掉即可跑通,175 个模型共用一个地址。