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.WithTimeout30–60 秒防止一次调用无限挂起
首包等待Transport.ResponseHeaderTimeout20 秒上游没起来时快速失败
连接建立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 的详细处理方式见限流专题。

请求体字段速查

字段类型什么时候必须填备注
modelstring总是名称以模型列表为准
messagesarray总是第一条可以是 system
max_tokensint长输出任务不填则服务端决定上限
temperaturefloat需要稳定输出时抽取任务建议 0–0.3
streambool前端实时显示开启后响应是 SSE
response_formatobject需要 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 也是共享的,任何一个卡住的请求都会影响其他调用方。

延伸阅读

Go 项目里最省事的做法是只写一次客户端封装,Key 从环境变量读。注册后创建 API Key,把上面代码里的 BaseURL 和 api_key 换掉即可跑通,175 个模型共用一个地址。

#go 调用 openai api#golang openai sdk#go-openai 用法#go 流式输出 api#golang 接入大模型
免费创建 API Key

相关阅读

Node.js 接入 OpenAI API 完整教程

Node.js 和 TypeScript 项目接大模型,官方 openai SDK 十几行代码就能跑通。这篇覆盖安装配置、baseURL 指向兼容网关、流式输出、超时重试和常见报错排查,附 Next.js 与 Express 两种落地写法。

Java 项目接入 OpenAI API 完整方案

Spring Boot 项目要接大模型,Spring AI 和官方 openai-java SDK 该选哪个?这篇给出两套可直接落地的配置与代码,讲清 base-url 拼接、流式输出、超时重试和 429 处理,附常见报错对照。