教程:使用Go和Gin开发RESTful API
本教程介绍了使用 Go 和 Gin Web Framework(Gin)编写 RESTful web 服务 API 的基本知识。
如果您对 Go 及其工具有基本的熟悉,您将充分利用本教程。如果这是你第一次接触 Go 语言,请参阅教程:快速入门。
Gin 简化了许多与构建 web 应用程序(包括 web 服务)相关的编码任务。在本教程中,您将使用 Gin 来创建路由请求、检索请求详细信息,并为响应封送 JSON。
在本教程中,您将构建一个具有两个端点的 RESTful API 服务器。您的示例项目将是一个关于老式爵士乐唱片的数据存储库。
本教程包括以下部分:
- 设计 API 接口。
- 为您的代码创建一个文件夹。
- 创建数据。
- 编写一个处理程序以返回所有项。
- 编写一个处理程序来添加一个新项。
- 编写一个处理程序以返回特定项。
注意:有关其他相关知识,请参见教程。
# 课程准备
Go 1.16 或更高版本的安装。有关安装说明,请参阅安装 Go。
用于编辑代码的工具。您拥有的任何文本编辑器都可以正常工作。
一种命令终端。Go 在 Linux 和 Mac 上以及 Windows 中的 PowerShell 或 cmd 上使用任何终端都能很好地工作。
cUrl 工具。在 Linux 和 Mac 上,应该已经安装了。在 Windows 上,它包含在 Windows 10 Insider 版本 17063 及更高版本中。对于早期的 Windows 版本,您可能需要安装它。有关更多信息,请参阅Tar And Curl Come to Windows (opens new window)。
# API 接口设计
您将构建一个 API,该 API 可以访问一家出售老式黑胶唱片的商店。因此,您需要提供接口,客户端可以通过这些接口为用户获取和添加相册。
在开发 API 时,通常从设计接口开始。如果接口易于理解,那么 API 的用户将获得更大的成功。
以下是您将在本教程中创建的接口。
/albums
- GET–获取所有相册的列表,以 JSON 形式返回。
- POST–从以 JSON 形式发送的请求数据中添加新相册。
/albums/{id}
- GET–通过相册 ID 获取相册,并以 JSON 形式返回相册数据。
接下来,您将为代码创建一个文件夹。
# 为代码创建文件夹
首先,为您将要编写的代码创建一个项目。
- 打开命令提示符并更改到主目录。
在 Linux 或 Mac 上:
cd
在 Windows 上:
cd %HOMEPATH%
- 使用命令提示符,为代码创建一个名为 web-service-gin 的目录。
mkdir web-service-gin
cd web-service-gin
创建一个可以在其中管理依赖关系的模块。
运行 go-mod-init 命令,为其提供代码所在模块的路径。
$ go mod init example.com/web-service-gin
go: creating new go.mod: module example.com/web-service-gin
此命令创建一个 go.mod 文件,您添加的依赖项将列在该文件中进行跟踪。有关详细信息,请参阅管理依赖关系 (opens new window)。
接下来,您将设计用于处理数据的数据结构。
# 创建数据
为了使教程保持简单,您将把数据存储在内存中。更深层课程的 API 将与数据库进行交互。
请注意,将数据存储在内存中意味着每次停止服务器时,相册集都会丢失,然后在启动服务器时重新创建。
# 编写代码
使用文本编辑器,在 web 服务目录中创建一个名为 main.go 的文件。您将在该文件中编写 Go 代码。
在 main.go 文件的顶部,粘贴以下包声明。
package main
独立程序(与库相对)始终位于程序包主目录中。
- 在包声明下面,使用以下代码来定义 相册 的 struct 数据结构。您将使用它将相册数据存储在内存中。
struct 标记,如 json:“artist”,指定当结构的内容被序列化为 json 时,字段的名称应该是什么。如果没有它们,JSON 将使用结构的大写字段名——这种风格在 JSON 中并不常见。
// album represents data about a record album.
type album struct {
ID string `json:"id"`
Title string `json:"title"`
Artist string `json:"artist"`
Price float64 `json:"price"`
}
- 在刚添加的结构声明下面,粘贴下面的相册结构片段,其中包含将用于启动的数据。
// albums slice to seed record album data.
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},
}
接下来,您将编写代码来实现您的第一个接口。
# 编写处理程序以返回所有项目
当客户端在 GET/albums 上发出请求时,您希望以 JSON 的形式返回所有相册。
为此,您将编写以下内容:
- 准备响应的逻辑
- 将请求路径映射到逻辑的代码
请注意,这与它们在运行时的执行方式相反,但您要先添加依赖项,然后添加依赖于它们的代码。
# 编写代码
- 在上一节中添加的结构代码下面,粘贴以下代码以获得相册列表。
这个 getAlbums 方法从相册结构的切片中创建 JSON,并将 JSON 写入响应中。
// getAlbums responds with the list of all albums as JSON.
func getAlbums(c *gin.Context) {
c.IndentedJSON(http.StatusOK, albums)
}
在此代码中,您:
- 编写一个获取 gin.Context 参数的 getAlbums 函数。请注意,您可以给这个函数取任何名称——Gin 和 Go 都不需要特定的函数名称格式。
- gin.Contex 是代码最重要的部分。它携带请求细节、验证和序列化 JSON 等等。(尽管名称相似,但这与 Go 的内置上下文包不同。)
- 调用 Context.IndetedJSON 将结构序列化为 JSON 并将其添加到响应中。
- 函数的第一个参数是要发送到客户端的 HTTP 状态代码。在这里,您从 net/http 包传递 StatusOK 常量,以指示 200 OK。
请注意,您可以将 Context.IndetedJSON 替换为对 Context.JSON 的调用,以发送更紧凑的 JSON。在实践中,缩进形式在调试时更容易使用,并且大小差异通常很小。
- 在 main.go 的顶部附近,就在相册切片声明的下方,粘贴下面的代码,将处理程序函数分配给端点路径。
这将建立一个关联,getAlbums 在该关联中处理对/albums 端点路径的请求。
func main() {
router := gin.Default()
router.GET("/albums", getAlbums)
router.Run("localhost:8080")
}
在此代码中,您:
- 使用 Default 方法初始化 Gin 路由器。
- 使用 GET 方式将 GET HTTP 方法和/相册路径与处理程序函数相关联。
请注意,您正在传递 getAlbums 函数的名称。这与传递函数的结果不同,后者是通过传递 getAlbums()来实现的(注意括号)。
- 使用 Run 方法将路由器连接到 http.Server 并启动服务器。
- 在 main.go 的顶部附近,就在包声明的下方,导入您需要支持刚刚编写的代码的包。
第一行代码应该如下所示:
package main
import (
"net/http"
"github.com/gin-gonic/gin"
)
- 保存 main.go 文件
# 运行代码
- 开始将 Gin 模块作为依赖项进行跟踪。
在命令行中,使用 go-get 添加 github.com/gin-gonic/gin 模块作为模块的依赖项。使用句点参数表示“获取当前目录中代码的依赖项”
$ go get .
go get: added github.com/gin-gonic/gin v1.7.2
Go 解析并下载了此依赖项,以满足您在上一步中添加的导入声明。
- 在包含 main.go 的目录中的命令行中,运行代码。使用句点参数表示“在当前目录中运行代码”
go run .
代码运行后,您就有了一个正在运行的 HTTP 服务器,可以向该服务器发送请求。
- 在一个新的命令行窗口中,使用 curl 向正在运行的 web 服务发出请求。
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
}
]
您已经启动了一个 API!在下一节中,您将创建另一个端点,其中包含处理 POST 请求以添加项目的代码。
# 编写处理程序以添加新项
当客户端在/相册上发出 POST 请求时,您希望将请求正文中描述的相册添加到现有相册数据中。
为此,您将编写以下内容:
- 将新相册添加到现有列表的逻辑。
- 将 POST 请求路由到您的逻辑的一段代码。
# 编写代码
- 添加代码以将相册数据添加到相册列表中。
在 import 语句之后的某个位置,粘贴以下代码。(文件的末尾是编写这段代码的好地方,但 Go 并没有强制执行声明函数的顺序。)
// postAlbums adds an album from JSON received in the request body.
func postAlbums(c *gin.Context) {
var newAlbum album
// Call BindJSON to bind the received JSON to
// newAlbum.
if err := c.BindJSON(&newAlbum); err != nil {
return
}
// Add the new album to the slice.
albums = append(albums, newAlbum)
c.IndentedJSON(http.StatusCreated, newAlbum)
}
在此代码中,您:
- 使用 Context.BindJSON 将请求正文绑定到 newAlbum。
- 将从 JSON 初始化的相册结构附加到相册切片。
- 在响应中添加一个 201 状态代码,以及表示您添加的相册的 JSON。
- 更改主函数,使其包含 router.POST 函数,如下所示。
func main() {
router := gin.Default()
router.GET("/albums", getAlbums)
router.POST("/albums", postAlbums)
router.Run("localhost:8080")
}
在此代码中,您:
- 将/albums 路径上的 POST 方法与 postAlbums 函数相关联。
- 使用 Gin,您可以将处理程序与 HTTP 方法和路径组合相关联。通过这种方式,您可以根据客户端使用的方法将发送到单个路径的请求单独路由。
# 运行代码
如果服务器仍在从最后一部分运行,请停止它。
在包含 main.go 的目录中的命令行中,运行代码。
$go run .
从另一个命令行窗口,使用 curl 向正在运行的 web 服务发出请求。
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}'
该命令应该为添加的相册显示标题和 JSON。
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
}
- 与上一节一样,使用 curl 检索相册的完整列表,您可以使用该列表来确认是否添加了新相册。
curl http://localhost:8080/albums \
--header "Content-Type: application/json" \
--request "GET"
该命令应显示相册列表。
[
{
"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
}
]
在下一节中,您将添加代码来处理特定项目的 GET。
# 编写处理程序以返回特定项
当客户端请求 GET/albums/[id]时,您希望返回 id 与 id 路径参数匹配的相册。
为此,您将:
- 添加逻辑以检索请求的相册。
- 将路径映射到逻辑。
# 编写代码
在上一节中添加的 postAlbums 函数下面,粘贴以下代码以检索特定的相册。
此 getAlbumByID 函数将提取请求路径中的 ID,然后找到匹配的相册。
// getAlbumByID locates the album whose ID value matches the id
// parameter sent by the client, then returns that album as a response.
func getAlbumByID(c *gin.Context) {
id := c.Param("id")
// Loop over the list of albums, looking for
// an album whose ID value matches the parameter.
for _, a := range albums {
if a.ID == id {
c.IndentedJSON(http.StatusOK, a)
return
}
}
c.IndentedJSON(http.StatusNotFound, gin.H{"message": "album not found"})
}
在此代码中,您:
使用 Context.Param 从 URL 中检索 id 路径参数。将此处理程序映射到路径时,将在路径中包含参数的占位符。
循环遍历切片中的相册结构,查找 ID 字段值与 ID 参数值匹配的结构。如果找到了,则将该相册结构序列化为 JSON,并将其作为响应返回,并返回一个 200 OK 的 HTTP 代码。
如上所述,真实世界的服务可能会使用数据库查询来执行此查找。
如果找不到相册,则返回 HTTP.StatusNotFound 的 HTTP 404 错误。
- 最后,更改 main,使其包含一个对 router.GET 的新调用,其中的路径现在是/albums/:id,如以下示例所示。
func main() {
router := gin.Default()
router.GET("/albums", getAlbums)
router.GET("/albums/:id", getAlbumByID)
router.POST("/albums", postAlbums)
router.Run("localhost:8080")
}
在此代码中,您:
- 将/albums/:id 路径与 getAlbumByID 函数相关联。在 Gin 中,路径中项目前面的冒号表示该项目是一个路径参数。
# 运行代码
如果服务器仍在从最后一部分运行,请停止它。
在包含 main.go 的目录中的命令行中,运行代码以启动服务器。
$go run .
从另一个命令行窗口,使用 curl 向正在运行的 web 服务发出请求。
curl http://localhost:8080/albums/2
该命令应该显示您使用其 ID 的相册的 JSON。如果没有找到相册,您将得到带有错误消息的 JSON。
{
"id": "2",
"title": "Jeru",
"artist": "Gerry Mulligan",
"price": 17.99
}
# 结论
祝贺您刚刚使用 Go 和 Gin 编写了一个简单的 RESTful web 服务。
# 建议的下一个主题
- 如果你是 Go 新手,你会在Effective Go (opens new window)和How to write Go code (opens new window)中找到有用的最佳实践。
- Go Tour (opens new window)是对 Go 基础知识的一个很好的循序渐进的介绍。
- 有关 Gin 的更多信息,请参阅Gin Web Framework package documentation (opens new window)文档或Gin Web Framework docs (opens new window)文档。
# 已完成代码
本节包含使用本教程构建的应用程序的代码。
package main
import (
"net/http"
"github.com/gin-gonic/gin"
)
// album represents data about a record album.
type album struct {
ID string `json:"id"`
Title string `json:"title"`
Artist string `json:"artist"`
Price float64 `json:"price"`
}
// albums slice to seed record album data.
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")
}
// getAlbums responds with the list of all albums as JSON.
func getAlbums(c *gin.Context) {
c.IndentedJSON(http.StatusOK, albums)
}
// postAlbums adds an album from JSON received in the request body.
func postAlbums(c *gin.Context) {
var newAlbum album
// Call BindJSON to bind the received JSON to
// newAlbum.
if err := c.BindJSON(&newAlbum); err != nil {
return
}
// Add the new album to the slice.
albums = append(albums, newAlbum)
c.IndentedJSON(http.StatusCreated, newAlbum)
}
// getAlbumByID locates the album whose ID value matches the id
// parameter sent by the client, then returns that album as a response.
func getAlbumByID(c *gin.Context) {
id := c.Param("id")
// Loop through the list of albums, looking for
// an album whose ID value matches the parameter.
for _, a := range albums {
if a.ID == id {
c.IndentedJSON(http.StatusOK, a)
return
}
}
c.IndentedJSON(http.StatusNotFound, gin.H{"message": "album not found"})
}