教程:使用 Go 和 Gin 开发 RESTful API
本教程介绍了使用 Go 和 Gin Web 框架 (Gin) 编写 RESTful Web 服务 API 的基础知识。
如果您对 Go 及其工具包有基本的了解,那么您将从本教程中获得最大的收益。如果您是第一次接触 Go,请参阅 教程:开始使用 Go 以快速了解。
Gin 简化了许多与构建 Web 应用程序(包括 Web 服务)相关的编码任务。在本教程中,您将使用 Gin 来路由请求、检索请求详细信息和编组 JSON 以进行响应。
在本教程中,您将构建一个具有两个端点的 RESTful API 服务器。您的示例项目将是关于老式爵士唱片数据的存储库。
本教程包括以下部分
- 设计 API 端点。
- 为你的代码创建一个文件夹。
- 创建数据。
- 编写一个处理程序以返回所有项目。
- 编写一个处理程序以添加一个新项目。
- 编写一个处理程序以返回一个特定项目。
注意:有关其他教程,请参阅 教程。
要尝试以在 Google Cloud Shell 中完成的交互式教程的形式进行此操作,请点击下面的按钮。
先决条件
- 安装 Go 1.16 或更高版本。有关安装说明,请参阅 安装 Go。
- 一个用于编辑代码的工具。你拥有的任何文本编辑器都可以正常工作。
- 一个命令终端。Go 在 Linux 和 Mac 上的任何终端上以及 Windows 中的 PowerShell 或 cmd 中都能很好地工作。
- curl 工具。在 Linux 和 Mac 上,此工具应该已经安装。在 Windows 上,它包含在 Windows 10 Insider 内部版本 17063 及更高版本中。对于较早的 Windows 版本,你可能需要安装它。有关更多信息,请参阅 Tar 和 Curl 已引入 Windows。
设计 API 端点
你将构建一个 API,该 API 提供对销售黑胶唱片的老式录音商店的访问权限。因此,你需要提供一个端点,客户端可以通过该端点为用户获取和添加专辑。
在开发 API 时,你通常从设计端点开始。如果端点易于理解,API 的用户将获得更大的成功。
以下是你将在本教程中创建的端点。
/albums
GET
– 以 JSON 格式获取所有专辑的列表。POST
– 从以 JSON 格式发送的请求数据中添加一个新专辑。
/albums/:id
GET
– 按 ID 获取一个专辑,并将专辑数据以 JSON 格式返回。
接下来,你将为你的代码创建一个文件夹。
为你的代码创建一个文件夹
首先,为你要编写的代码创建一个项目。
-
打开一个命令提示符并切换到你的主目录。
在 Linux 或 Mac 上
$ cd
在 Windows 上
C:\> cd %HOMEPATH%
-
使用命令提示符,为你的代码创建一个名为 web-service-gin 的目录。
$ mkdir web-service-gin $ cd web-service-gin
-
创建一个模块,你可以在其中管理依赖项。
运行
go mod init
命令,并向其提供你的代码所在的模块的路径。$ go mod init example/web-service-gin go: creating new go.mod: module example/web-service-gin
此命令会创建一个 go.mod 文件,您添加的依赖项将列在其中以进行跟踪。有关使用模块路径命名模块的详细信息,请参阅管理依赖项。
接下来,您将设计数据结构来处理数据。
创建数据
为了使教程简单易懂,您将数据存储在内存中。更典型的 API 会与数据库进行交互。
请注意,将数据存储在内存中意味着每次停止服务器时都会丢失专辑集,然后在启动服务器时重新创建。
编写代码
-
使用文本编辑器,在 web-service 目录中创建一个名为 main.go 的文件。您将在该文件中编写 Go 代码。
-
在 main.go 中,将以下包声明粘贴到文件顶部。
package main
一个独立程序(与库相对)始终位于包
main
中。 -
在包声明下方,粘贴以下
album
结构的声明。您将使用它来将专辑数据存储在内存中。诸如
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"` }
-
在您刚刚添加的结构声明下方,粘贴以下包含您将用来开始的
album
结构的切片。// 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
函数从album
结构的切片创建 JSON,并将 JSON 写入响应。// getAlbums responds with the list of all albums as JSON. func getAlbums(c *gin.Context) { c.IndentedJSON(http.StatusOK, albums) }
在此代码中,您
-
编写一个
getAlbums
函数,它采用gin.Context
参数。请注意,您可以为该函数指定任何名称——Gin 和 Go 都不需要特定的函数名称格式。gin.Context
是 Gin 中最重要的部分。它承载请求详细信息、验证和序列化 JSON 等。(尽管名称相似,但这与 Go 内置的context
包不同。) -
调用
Context.IndentedJSON
将结构序列化为 JSON 并将其添加到响应中。该函数的第一个参数是要发送给客户端的 HTTP 状态代码。在此处,您传递的是来自
net/http
包的StatusOK
常量,以指示200 OK
。请注意,您可以用对
Context.JSON
的调用替换Context.IndentedJSON
以发送更紧凑的 JSON。实际上,缩进形式在调试时更容易使用,并且大小差异通常很小。
-
-
在 main.go 的顶部附近,就在
albums
切片的声明下方,粘贴以下代码,将处理程序函数分配给端点路径。这设置了一个关联,其中
getAlbums
处理对/albums
端点路径的请求。func main() { router := gin.Default() router.GET("/albums", getAlbums) router.Run("localhost:8080") }
在此代码中,您
-
在 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 解析并下载此依赖项以满足您在上一步中添加的
import
声明。 -
在包含 main.go 的目录中的命令行中,运行代码。使用点参数表示“运行当前目录中的代码”。
$ go run .
代码运行后,您将拥有一个正在运行的 HTTP 服务器,您可以向其发送请求。
-
从一个新的命令行窗口中,使用
curl
向正在运行的 Web 服务发出请求。$ curl https://127.0.0.1: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
请求以添加一个项目。
编写一个处理程序以添加一个新项目
当客户端在 /albums
处发出 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 初始化的
album
结构追加到albums
切片。 - 向响应中添加
201
状态代码,以及表示您添加的专辑的 JSON。
- 使用
-
更改您的
main
函数,使其包含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 https://127.0.0.1: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 https://127.0.0.1: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
路径参数。将此处理程序映射到路径时,您将在路径中包含一个占位符以供参数使用。 -
遍历切片中的
album
结构,查找其中ID
字段值与id
参数值匹配的一个。如果找到,您将把该album
结构序列化为 JSON,并以200 OK
HTTP 代码作为响应返回。如上所述,实际服务可能使用数据库查询来执行此查找。
-
如果未找到专辑,请返回 HTTP
404
错误,其中包含http.StatusNotFound
。
-
-
最后,更改您的
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 https://127.0.0.1:8080/albums/2
该命令应显示您使用的 ID 的专辑的 JSON。如果未找到专辑,您将获得带有错误消息的 JSON。
{ "id": "2", "title": "Jeru", "artist": "Gerry Mulligan", "price": 17.99 }
结论
恭喜!您刚刚使用 Go 和 Gin 编写了一个简单的 RESTful Web 服务。
建议的后续主题
- 如果您是 Go 新手,您将在 有效的 Go 和 如何编写 Go 代码 中找到有用的最佳实践。
- Go Tour 是一个循序渐进的 Go 基础知识入门。
- 有关 Gin 的更多信息,请参阅 Gin Web Framework 包文档 或 Gin Web Framework 文档。
已完成的代码
本部分包含您使用本教程构建的应用程序的代码。
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"})
}