Back to Blogs

Tech / Tutorial

前后端交互工程实践指南 (Vol.4:外部 API 接入)

TechTutorial
前后端交互GoAPILLM

本文档建立在 Vol.1、Vol.2 与 Vol.3 之上,还未完成前序内容的请先完成Vol.1Vol.2Vol.3 前序教程。通过之前的实践,我们已经完成了使用 Gin 框架并结合 Nginx 代理搭建网站的基础架构。

在真实开发场景中,后端开发需要编写很多个API,但人力终究是有限的,不可能所有的 API 都在后端手写完成;特别是在调用 LLM(大型语言模型)服务时,我们也不可能在本地手搓一个大模型。所以,我们需要让后端自己去调用外部的 API 。本文档以调用 Gemini API 为例,实战搭建一个 AI 聊天网站,后端语言为 Go,前端为 HTML。本文会有概念与实操,按文档进行以获得最佳效果。

模块一:核心概念

1. 什么是调用外部 API?

之前我们写的 API,是用户(浏览器)向我们的服务器发请求,由我们的服务器接收请求并返回结果 。而“调用外部 API”,则是将我们的服务器(Go 后端)成为另一个客户端,从而去向第三方的服务器发送请求 。

在本例中,为什么不让前端直接去调用 AI 大模型的API?

  • 安全风险:调用外部 AI 通常需要一个极其机密的密码(API Key) 。如果让前端去调用,这个密码就会明文暴露在前端代码里,任何人只要按 F12 打开 DevTools 就能偷走密钥,去倒刷你的额度,直到彻底耗尽 。
  • 无视跨域:同源策略是浏览器的单方面限制。服务器和服务器之间互相发请求,是没有任何跨域限制的 !所以我们的 Go 后端去请求外部服务,不需要再去配置复杂的跨域中间件。

2. 什么是 OpenAI 格式?

现在市面上有无数的大模型(如 ChatGPT、DeepSeek 等)。为了不让程序员每接入一个新模型就要重写一遍代码,业界逐渐形成了一个默认的“行业标准”——OpenAI API 格式

它的核心交互格式非常直白,本质上就是一段 JSON,包含模型名字(model)和聊天记录(messages):

{
  "model": "gpt-3.5-turbo",
  "messages": [
    {"role": "user", "content": "你好,请自我介绍"}
  ]
}
  • role(角色)user 代表你(用户),assistant 代表 AI 。
  • content(内容):具体说了什么话 。

但是值得注意的是,谷歌提供的API格式是与OpenAI API不同的,最近才开放兼容OpenAI API。故本例依旧使用OpenAI API标准格式。

3. Cloudflare:全球网络的中转枢纽

因为网络防火墙和跨国调用的延迟问题,国内服务器直接请求国外的 AI 接口经常会遇到 i/o timeout 。为了能顺利完成本教程,我已经配置好了基于 Cloudflare 的代理,你可以通过直连 https://gobackapi.xyz 转发请求 。

Cloudflare 是什么?

它是全球最大的网络安全和 CDN(内容分发网络)服务商,并免费提供 DNS 服务。你可以把它理解为一个遍布全球的高速中转站。

  • DNS:将域名(如 imaginary.com)转化为 IP 地址(如 111.11.111.1)的解析系统。
  • CDN 的作用:客户端与服务器的物理距离越远,连接越慢甚至超时,导致极差的用户体验 。部署在全球数据中心的 CDN 节点会缓存静态资源,让用户直接从网络距离最近的节点读取数据,大幅加快加载速度并减少服务器压力。不过需要注意的是,大模型对话属于动态处理,无法通过 CDN 缓存,最终仍需经过 Cloudflare 路由到模型原生服务器 。

模块二:实战模拟

我们将 Vol.1 中的前后端交互实例 2进行升级,把后端服务从原生逻辑改进为接入 Gemini 模型 。

当前的系统架构流转应当是: 前端(浏览器) --> Nginx --> Go 服务器 --> Cloudflare 代理 --> 谷歌服务器

Step 1: 建立目录结构

go-demo1/
├── main.go
└── index.html

Step 2: 编写后端代码main.go

将 Go 服务器转化为请求第三方接口的客户端。

package main

import (
    "bytes"
    "encoding/json"
    "fmt"
    "io"
    "net/http"

    "github.com/gin-gonic/gin"
)

// FrontendRequest 定义前端浏览器发给我们的格式
// 比如前端传过来 {"message": "你好"}
type FrontendRequest struct {
    Message string `json:"message"`
}

// OpenAIMessage 定义单条对话的格式
type OpenAIMessage struct {
    Role    string `json:"role"`    // 角色:你是 "user",AI 是 "assistant"
    Content string `json:"content"` // 具体的文字内容
}

// OpenAIRequest 定义我们要发给外部 AI (Gemini) 的整体格式
// 必须严格遵守 OpenAI 的标准规范
type OpenAIRequest struct {
    Model    string          `json:"model"`    // 告诉 AI 我们要用哪个模型
    Messages []OpenAIMessage `json:"messages"` // 把聊天记录打包传过去
}

// OpenAIResponse 定义外部 AI 返回给我们的复杂格式
// 我们只需要从这个复杂的结构中找到我们要的回复内容
type OpenAIResponse struct {
    Choices []struct {
        Message OpenAIMessage `json:"message"`
    } `json:"choices"`
}


func main() {
    // 创建一个默认的 Gin 路由引擎 (相当于启动我们的 Web 服务器)
    r := gin.Default()

    // 开放一个 POST 接口,前端浏览器请求 /api/chat 时,就会执行下面大括号里的代码
    r.POST("/api/chat", func(c *gin.Context) {

        // 1. 解析前端发送的数据
        var req FrontendRequest
        // 尝试把前端发来的 JSON 数据解析到我们的结构体里
        // 如果前端发来的格式不对,就报错并返回 400 状态码
        if err := c.ShouldBindJSON(&req); err != nil {
            c.JSON(400, gin.H{"error": "参数解析失败"})
            return
        }
        fmt.Println("收到前端提问:", req.Message)

        // 2. 整理我们要请求第三方API的内容
        aiReqData := OpenAIRequest{
            // 使用最新、免费且速度极快的 flash 模型
            Model: "gemini-3-flash-preview", 
            Messages: []OpenAIMessage{
                // 把前端刚才说的话,以 user (用户) 的身份塞进去
                {Role: "user", Content: req.Message},
            },
        }

        // 把我们组装好的数据打包为 JSON 
        requestBody, _ := json.Marshal(aiReqData)

        // 3. 向第三方API发送请求
        // 必须使用 Google 官方的 OpenAI 兼容路径:/v1beta/openai/chat/completions
        apiURL := "https://gobackapi.xyz/v1beta/openai/chat/completions"

        // 准备一封信 (POST 请求),贴上地址 (apiURL),装入信件内容 (requestBody)
        outReq, _ := http.NewRequest("POST", apiURL, bytes.NewBuffer(requestBody))

        // 4. 在信封上写明注意事项 (设置 HTTP 请求头)
        outReq.Header.Set("Content-Type", "application/json") // 告诉对方我们寄的是 JSON 数据

        // 填入 Gemini API Key,注意保留 "Bearer " 前缀和空格!
        // 这一步就是服务器代替前端,向外部服务器出示通行密码
        outReq.Header.Set("Authorization", "Bearer YOUR_API_KEY_HERE") 

        // 5. 正式发送请求
        client := &http.Client{}
        resp, err := client.Do(outReq) // Do 方法会一直等,直到 AI 把话说完传回来

        // 如果网络断开或者服务器死机,给前端报个 500 错误
        if err != nil {
            c.JSON(500, gin.H{"error": "连接 AI 接口失败"})
            return
        }
        // 无论发生什么,在这个功能结束前,必须挂断和外部 API 的连线,防止内存泄漏
        defer resp.Body.Close()

        // 6. 解析第三方 API (AI) 返回的内容
        bodyBytes, _ := io.ReadAll(resp.Body) // 读取全部原始数据

        // 打印API完整内容,用于调试
        fmt.Println("API 原始返回内容:", string(bodyBytes))

        // 创建一个空盒子,准备装解析后的数据
        var aiResp OpenAIResponse
        // 把 JSON 格式的响应解压到我们的盒子 (OpenAIResponse 结构体) 里
        json.Unmarshal(bodyBytes, &aiResp)

        // 7. 提取出最核心的 AI 回复文本
        replyText := "AI没有返回内容"

        // 为了防止程序崩溃 (如果 AI 报错,Choices 可能是空的,直接去取就会报错),先检查一下长度
        if len(aiResp.Choices) > 0 {
            // 一层一层剥开结构体,拿到具体的文本
            replyText = aiResp.Choices[0].Message.Content
        }

        fmt.Println("AI 返回结果:", replyText)

        // 8. 把提取出来的文本转交给前端浏览器
        c.JSON(200, gin.H{
            "reply": replyText,
        })
    })

    // 启动服务器,让它保持运行并监听 8080 端口
    fmt.Println(">>> Gin 后端已启动!监听端口: 8080")
    r.Run(":8080")
}

Step 3: 获取 API Key

这里对特定读者提供我自己的 API Key,请保护好Key,任何时候不要明文写在前端页面,也不要泄漏。点击下方按钮进行密码验证,即可提取专属 API Key 。

访问控制区

此 Key 已被加密,请输入邀请码解锁。

也可访问https://aistudio.google.com/api-keys获取属于你的API Key。

Step 4: 编写前端代码 index.html

这里的前端只负责向我们同源的 Nginx 发请求,完全不知道外部 API 的存在。

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <title>第三方API交互示例</title>
    <style>
        /* 简单美化:让界面看起来清爽 */
        body { font-family: 'PingFang SC', sans-serif; padding: 40px; background: #f5f5f5; line-height: 1.6; }
        .container { max-width: 600px; margin: 0 auto; background: white; padding: 25px; border-radius: 12px; box-shadow: 0 4px 10px rgba(0,0,0,0.1); }
        input { padding: 10px; width: 70%; border: 1px solid #ddd; border-radius: 4px; }
        button { padding: 10px 20px; background: #007bff; color: white; border: none; border-radius: 4px; cursor: pointer; }
        .result-box { margin-top: 20px; padding: 15px; border-radius: 6px; font-size: 13px; background: #f8f9fa; border-left: 5px solid #343a40;}
        .success-text { color: #333; }
        .error-text { color: #cf1322; font-weight: bold; }
    </style>
</head>
<body>
    <div class="container">
        <h2>调用 AI 示例</h2>
        <div style="margin-top: 20px;">
            <input type="text" id="chatInput" placeholder="今天在想些什么?">
            <button onclick="sendToAI()">发送</button>
        </div>

        <div class="result-box">
            <p id="chatResult" class="success-text">你好,今天心情怎么样?</p>
        </div>
    </div>

<script>
    // 云服务器公网 IP
    const SERVER_IP = "47.101.141.52";

    async function sendToAI() {
        const msg = document.getElementById('chatInput').value;
        if (!msg.trim()) {
            alert("请输入内容后再发送!");
            return;
        }

        const resultBox = document.getElementById('chatResult');
        resultBox.innerText = " 正在思考中...";
        resultBox.className = "success-text"; // 重置为正常颜色

        // 请求我们同源的 Nginx 代理地址 (888 端口)
        const url = `http://${SERVER_IP}:888/api/chat`;

        try {
            const response = await fetch(url, {
                method: 'POST',
                headers: { 'Content-Type': 'application/json' },
                body: JSON.stringify({ message: msg }),
            });

            const data = await response.json();

            // 处理后端的 400/500 等 HTTP 错误
            if (!response.ok) {
                resultBox.innerText = `请求报错:${data.error || '未知错误'}`;
                resultBox.className = "error-text";
                return;
            }

            // 处理截图中的“AI没有返回内容”逻辑
            if (data.reply === "AI没有返回内容" || data.reply === "") {
                resultBox.innerText = "出了点问题,没有返回任何内容 (可能是 API Key 错误或额度耗尽)。";
                resultBox.className = "error-text";
            } else {
                // 正常返回 AI 的回答
                resultBox.innerText = data.reply;
                resultBox.className = "success-text";
            }

        } catch (error) {
            // 处理网络断开或 Nginx 没开等极狐错误
            resultBox.innerText = "前端网络请求失败,请检查 Nginx 或网络连接!";
            resultBox.className = "error-text";
            console.error(error);
        }
    }
</script>
</body>
</html>

Step 5: 部署与访问

启动 Go 服务并确保 Nginx 正常运行,访问 http://47.101.141.52:888/。至此,使用 Nginx 代理部署到服务器上、属于你的第一个调用第三方 API 的网站就正式上线了!


END