用 Go 与 Gin 开发 RESTful API:从唱片列表到新增与按 ID 查询

原文作者:Go 文档团队。中文译写与编辑整理:未完纪。

这篇教程从一个爵士黑胶唱片目录出发,用 Go 与 Gin 做一个小型 RESTful Web 服务。Gin 负责把请求送到相应处理器、读取路径和请求体,并把 Go 数据序列化为 JSON。完成后,客户端可以获取全部唱片、增加一张唱片,或按 ID 查询。

本文依据 Go 官方完整教程 译写,保留原文步骤、所有实质示例与最后的完整程序。它使用内存切片以突出 HTTP 处理过程;服务停止后,新增数据会消失。本文没有执行示例,以下结果均为原文展示或明确标出的预期行为。

客户端请求由Gin按方法和路径分发;处理器从JSON或路径中取值,读取或追加内存albums切片,再返回200、201或404 JSON响应。
唱片 API 的请求与数据流(原创技术示意图)

准备环境并确定端点

你需要对 Go 及其命令行工具有基本认识;若是第一次接触,可先阅读 Go 入门教程。准备一个 Go 环境、文本编辑器、命令终端和 curl。原文建议使用当前 Go 版本;Go 可在 Linux、macOS、Windows PowerShell 或 cmd 中使用。Windows 的 curl 是否可用还应以本机环境为准。

先确定 API 的使用方式,再写处理逻辑。教程中的两个路径对应三种方法与路径组合:

方法与路径 用途 正常响应
GET /albums 取回所有唱片 200 OK,JSON数组
POST /albums 用JSON请求体新增唱片 201 Created,新唱片对象
GET /albums/:id 按ID查询一张唱片 200 OK,对象;未找到则404

:id 是 Gin 路由定义中的参数位置。客户端不会把冒号发送给服务,而是请求 /albums/2 这样的具体路径。本教程不包含更新和删除端点。

创建目录与 Go 模块

在终端进入自己的工作目录,然后创建项目目录。原文先切换到用户主目录:Linux/macOS 可使用 cd,Windows cmd 可使用 cd %HOMEPATH%。若使用 PowerShell,应使用其自身的路径语法,例如 Set-Location $HOME,不要直接照搬 cmd 的百分号变量。接下来执行:

mkdir web-service-gin
cd web-service-gin
go mod init example/web-service-gin

go mod init 创建 go.mod,后续依赖会记录在其中。example/web-service-gin 是教程中的模块路径。若需要了解实际项目如何命名模块,可继续阅读 依赖管理文档。

设计数据结构与初始数据

在项目目录新建 main.go,第一行写 package main。独立可执行程序使用 main 包;这里不把它做成库。随后定义一张唱片的数据:

package main

type album struct {
    ID     string  `json:"id"`
    Title  string  `json:"title"`
    Artist string  `json:"artist"`
    Price  float64 `json:"price"`
}

ID、Title 等导出字段以大写字母开头,而字段标签 json:"artist" 决定序列化后的 JSON 键名。没有标签时,JSON 将使用 Go 字段原名。Price 在教程中采用 float64;真实货币业务需要另行定义精确数值与舍入规则。

接着加入用于启动演示的三张唱片:

var albums = []album{
    {ID: "1", Title: "Blue Train", Artist: "John Coltrane", Price: 56.99},
    {ID: "2", Title: "Jeru", Artist: "Gerry Mulligan", Price: 17.99},
    {ID: "3", Title: "Sarah Vaughan and Clifford Brown", Artist: "Sarah Vaughan", Price: 39.99},
}

数据只保存在当前进程内。每次重新启动,程序重新执行这段初始化,恢复到这三条种子数据。通常真实 API 会连接数据库;内存存储是本教程的简化安排。

先返回全部唱片

对 GET /albums,先编写构造响应的处理器,再把路由映射到它。编写顺序与运行时调用顺序相反:先准备被依赖的函数,再注册调用它的路由。

func getAlbums(c *gin.Context) {
    c.IndentedJSON(http.StatusOK, albums)
}

处理器接收 *gin.Context。它携带请求细节,并提供 JSON 绑定、响应写入等能力;它与 Go 标准库的 context 包不是同一个类型。函数名可以自由选择,Gin 并不要求处理器必须叫 getAlbums。

IndentedJSON 的第一个参数是 HTTP 状态码,http.StatusOK 对应 200;第二个参数是要编码的值。这里把 albums 序列化为易读的缩进 JSON。也可以使用 c.JSON 输出更紧凑的 JSON,接口数据语义不因此改变。

现在增加 main 函数,创建路由并启动监听:

func main() {
    router := gin.Default()
    router.GET("/albums", getAlbums)
    router.Run("localhost:8080")
}

gin.Default() 初始化默认路由器;router.GET 把 GET 方法与路径关联到处理函数。这里传递的是 getAlbums 函数本身,不能写成 getAlbums()——后者表示立即调用函数并传入结果。Run 将路由器附到 HTTP 服务并开始监听。

在 package main 下面补上依赖导入:

import (
    "net/http"
    "github.com/gin-gonic/gin"
)

安装依赖、启动并请求

保存文件后,在含有 main.go 的目录中添加依赖并运行:

go get .
go run .

点号表示当前目录中的包。go get . 根据导入声明解析依赖;原文示例输出曾显示 Gin v1.7.2,这是历史演示输出,不能当作本次下载或应固定使用的版本。实际项目要检查 go.mod、go.sum 与兼容性,并使用受控依赖版本。

服务运行时,在另一个终端请求:

curl http://localhost:8080/albums

原文展示的响应内容如下,此处只调整了排版:

[
  {"id": "1", "title": "Blue Train", "artist": "John Coltrane", "price": 56.99},
  {"id": "2", "title": "Jeru", "artist": "Gerry Mulligan", "price": 17.99},
  {"id": "3", "title": "Sarah Vaughan and Clifford Brown", "artist": "Sarah Vaughan", "price": 39.99}
]

接收 JSON 并新增唱片

客户端对 /albums 发出 POST 时,请求体描述一张新唱片。处理器先把 JSON 绑定到 album,成功后追加进切片,再以 201 状态返回新增对象:

func postAlbums(c *gin.Context) {
    var newAlbum album
    if err := c.BindJSON(&newAlbum); err != nil {
        return
    }
    albums = append(albums, newAlbum)
    c.IndentedJSON(http.StatusCreated, newAlbum)
}

BindJSON(&newAlbum) 接收一个指向结构体的指针,把解析出的字段写进去。出现绑定错误时,处理器立刻返回;Gin 的这一类绑定方法已经负责终止请求并设置错误响应,不能把这个 return 误解为所有错误都变成空的 200。若希望统一业务错误的 JSON 结构,应另外设计错误处理策略。

绑定成功以后,append 得到更新后的切片,赋回全局 albums。http.StatusCreated 对应 201,响应体是刚添加的对象。要让该函数收到 POST 请求,给 main 增加一条注册:

func main() {
    router := gin.Default()
    router.GET("/albums", getAlbums)
    router.POST("/albums", postAlbums)
    router.Run("localhost:8080")
}

同一路径可以根据 HTTP 方法分派给不同处理器,所以 GET 与 POST 都使用 /albums 并不冲突。

发送新增请求并重新查询

先停止上一阶段正在运行的服务,再执行 go run .。在另一终端发送以下请求。这个多行形式使用 POSIX shell 的反斜杠续行:

curl http://localhost:8080/albums \
    --include \
    --header "Content-Type: application/json" \
    --request "POST" \
    --data '{"id": "4","title": "The Modern Sound of Betty Carter","artist": "Betty Carter","price": 49.99}'

--include 让 curl 同时显示响应头。原教程返回 201 与新增唱片;其中响应日期与长度是当时请求的示例,不是固定协议值:

HTTP/1.1 201 Created
Content-Type: application/json; charset=utf-8
Date: Wed, 02 Jun 2021 00:34:12 GMT
Content-Length: 116

{
    "id": "4",
    "title": "The Modern Sound of Betty Carter",
    "artist": "Betty Carter",
    "price": 49.99
}

Windows 编辑补充:不同 PowerShell 版本对原生命令参数与引号的处理可能不同。可以把上面对象保存为 UTF-8 的 album.json,再用一行命令发送文件,避免把 POSIX 续行符粘进去:

curl.exe http://localhost:8080/albums --include --header "Content-Type: application/json" --request POST --data-binary "@album.json"

再次查询列表以确认新增数据:

curl http://localhost:8080/albums \
    --header "Content-Type: application/json" \
    --request "GET"

原文的列表此时多出第 4 张唱片。GET 请求没有 JSON 请求体,因此这里的 Content-Type 头并非完成查询所必需;保留它是为了对应原始演示。

[
  {"id": "1", "title": "Blue Train", "artist": "John Coltrane", "price": 56.99},
  {"id": "2", "title": "Jeru", "artist": "Gerry Mulligan", "price": 17.99},
  {"id": "3", "title": "Sarah Vaughan and Clifford Brown", "artist": "Sarah Vaughan", "price": 39.99},
  {"id": "4", "title": "The Modern Sound of Betty Carter", "artist": "Betty Carter", "price": 49.99}
]

按 ID 查询单张唱片

最后实现 GET /albums/:id。通过 c.Param("id") 取出路径参数,遍历切片寻找相同 ID;找到后立即写回对象并返回,避免继续执行“未找到”的分支。

func getAlbumByID(c *gin.Context) {
    id := c.Param("id")
    for _, a := range albums {
        if a.ID == id {
            c.IndentedJSON(http.StatusOK, a)
            return
        }
    }
    c.IndentedJSON(http.StatusNotFound, gin.H{"message": "album not found"})
}

如果找遍切片也没有结果,使用 http.StatusNotFound 返回 404。gin.H 是方便构造 JSON 对象的映射类型,这里生成 {"message":"album not found"}。实际数据规模增长以后,通常会把遍历换成数据库查询。

把最后一条路由注册进 main:

func main() {
    router := gin.Default()
    router.GET("/albums", getAlbums)
    router.GET("/albums/:id", getAlbumByID)
    router.POST("/albums", postAlbums)
    router.Run("localhost:8080")
}

停止旧服务并重新运行后,请求 ID 为 2 的唱片:

curl http://localhost:8080/albums/2
{
    "id": "2",
    "title": "Jeru",
    "artist": "Gerry Mulligan",
    "price": 17.99
}

如果传入不存在的 ID,响应应为 404 与未找到消息。这里说明的是代码分支的预期结果,并非本次向运行中的服务发送请求的记录。

完整程序

下面把上述部分放到一起,对应原文的 Completed code。只翻译了注释、调整空白,核心控制流与教学用存储方式不变。原文对 router.Run 的返回错误没有处理,相关补充放在下一节。

package main

import (
    "net/http"
    "github.com/gin-gonic/gin"
)

// 一张唱片的数据。
type album struct {
    ID     string  `json:"id"`
    Title  string  `json:"title"`
    Artist string  `json:"artist"`
    Price  float64 `json:"price"`
}

// 启动时的种子数据。
var albums = []album{
    {ID: "1", Title: "Blue Train", Artist: "John Coltrane", Price: 56.99},
    {ID: "2", Title: "Jeru", Artist: "Gerry Mulligan", Price: 17.99},
    {ID: "3", Title: "Sarah Vaughan and Clifford Brown", Artist: "Sarah Vaughan", Price: 39.99},
}

func main() {
    router := gin.Default()
    router.GET("/albums", getAlbums)
    router.GET("/albums/:id", getAlbumByID)
    router.POST("/albums", postAlbums)
    router.Run("localhost:8080")
}

// 返回全部唱片。
func getAlbums(c *gin.Context) {
    c.IndentedJSON(http.StatusOK, albums)
}

// 从 JSON 请求体中新增唱片。
func postAlbums(c *gin.Context) {
    var newAlbum album
    if err := c.BindJSON(&newAlbum); err != nil {
        return
    }
    albums = append(albums, newAlbum)
    c.IndentedJSON(http.StatusCreated, newAlbum)
}

// 按路径中的 ID 查找唱片。
func getAlbumByID(c *gin.Context) {
    id := c.Param("id")
    for _, a := range albums {
        if a.ID == id {
            c.IndentedJSON(http.StatusOK, a)
            return
        }
    }
    c.IndentedJSON(http.StatusNotFound, gin.H{"message": "album not found"})
}

从教学示例继续向前时

最需要注意的是共享切片。Gin 的 HTTP 处理器可能并发运行,而原示例中的列表查询、ID 遍历与 append 没有同步。即使本地按顺序执行 curl 看起来正常,也不能据此认为并发安全。真实服务应使用具备并发与一致性控制的数据库,或在明确的存储抽象中正确使用同步;仅在写入时加锁、读取时不加锁并不能解决问题。

其次,JSON 能解码不代表输入符合业务要求。当前模型未验证 ID 是否为空或重复、标题和艺术家是否为空、价格是否在有效范围内,也没有身份验证、授权、请求体大小限制与速率限制。应用需要自己定义这些约束。示例保留本机 localhost:8080 监听,不应为了远程试用就直接暴露成公开接口。

服务生命周期也需要补齐。以下只是编辑补充的启动错误检查形式:在 import 中增加 log,并把原来的 router.Run 调用替换成这段;它不代替 HTTP 超时、优雅停机或其他部署配置。

if err := router.Run("localhost:8080"); err != nil {
    log.Fatal(err)
}

完成这条入门路线后,可以继续阅读 Effective Go、How to write Go code、A Tour of Go 以及 Gin API 文档。原教程希望先让请求、路由、处理器与 JSON 的关系清楚,再进入完整应用的工程问题。

来源与许可

原文作者为 Go 文档团队,来源 Tutorial: Developing a RESTful API with Go and Gin。本文按 2026-10-05 读取的全文译写,补充了Windows命令差异、版本说明和静态审核边界,未执行文章代码。按 Go 网站版权页,除另有说明外,正文使用 CC BY 4.0 许可,代码使用网站链接的 BSD 许可。原始代码版权与BSD许可全文保留于本文末尾。示意图为本次原创。

版权与许可全文

以下保留本页涉及的来源材料或示例代码的版权、许可条件与免责声明;各自适用范围依原声明。中文翻译及编辑标注:未完纪,2026-10-05。

LICENSE.txt

Copyright 2009 The Go Authors.

Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are
met:

   * Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
   * Redistributions in binary form must reproduce the above
copyright notice, this list of conditions and the following disclaimer
in the documentation and/or other materials provided with the
distribution.
   * Neither the name of Google LLC nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享
评论 抢沙发

请登录后发表评论

    暂无评论内容